SecureRandom
is a great library for creating cryptographically secure random values in Ruby, and you should definitely use it in your application code where appropriate.
However, using SecureRandom
when writing tests for your Ruby code can make your tests harder to debug and more prone to flakiness.
The Importance of Deterministic Tests
Deterministic tests are tests that produce consistent results each time they are executed, given the same input. You might have noticed that testing libraries like RSpec
and MiniTest
allow you to run your tests with a given seed value in order to reproduce errors or flaky failures.
For example:
$ rspec spec --seed 1234
Passing in this seed value is helpful when debugging tests that only fail sometimes because it allows you to set up your test environment to be almost identical to how it was when that test last failed. Behind the scenes, this sets Kernel::srand
to the given seed value, making the output of the test deterministic.
Deterministic tests allow you to reproduce and investigate issues consistently. When a test fails, having a predictable outcome enables easier debugging and troubleshooting.
Deterministic tests also make it easier to collaborate with other developers. Since the tests generate consistent results, team members can share and compare their findings accurately across different development environments. This is particularly helpful if the failing tests occur as part of a Continuous Deployment test run because you can recreate the failures locally and fix them more easily.
Why Avoid SecureRandom in Tests
While SecureRandom
is a great tool for generating cryptographically secure random values, it may not be suitable for use within tests due to its non-deterministic nature. Here’s why…
SecureRandom
relies on unpredictable system-specific data to generate random numbers. This means that the output of SecureRandom
is still different each time it’s called, even when seeding Kernel with a given seed. As a result, using SecureRandom
within tests makes it difficult to reproduce the exact sequence of random numbers. In other words, using SecureRandom
makes tests non-deterministic and thus harder to debug.
What’s the Alternative? Use Random
Instead of using SecureRandom
, we can use the Random
class in Ruby in tests and in factories. By providing a seed, we can ensure that Random
generates the same sequence of random values for subsequent test runs, making the tests deterministic.
# BAD
it 'persists the Order' do
order = Order.new(number: SecureRandom.alphanumeric(12))
order.save
expect(order).to be_persisted
end
# GOOD
it 'persists the Order' do
order = Order.new(number: Random.alphanumeric(12))
order.save
expect(order).to be_persisted
end
Try It Yourself
To demonstrate the differences between Random and SecureRandom
in a testing scenario, consider the following example written in RSpec. The code will print the values of Random
and SecureRandom
to STDOUT
, allowing readers to observe the effects of seeding:
# spec_helper
RSpec.configure do |config|
Kernel.srand config.seed
end
# test_file.rb
require 'spec_helper'
require 'securerandom'
RSpec.describe "Random" do
describe '#uuid' do
it "puts the same UUID each time" do
puts "Random: #{Random.uuid}"
expect(true).to be_truthy
end
end
end
RSpec.describe "SecureRandom" do
describe '#uuid' do
it "puts a different UUID each time" do
puts "SecureRandom: #{SecureRandom.uuid}"
expect(true).to be_truthy
end
end
end
Try running this a few times and comparing the printed values…
$ rspec test_file.rb --seed 1234