brandon.hornseth

How Randomized Test Seeds Work In Rails

tech

Rails 5.0 changed the default order of its test suites from :sorted to :random. This change is meant to encourage best practices and ensure your tests don’t have order dependencies, which could hide bugs. You can see part of this behavior in the test suite output:

~$ bin/rails test
Run options: --seed 44656

# Running:
.....
 
 Finished in 0.027476s, 36.3952 runs/s, 72.9237 assertions/s.
  
  5 runs, 12 assertions, 0 failures, 0 errors, 0 skips

When running tests, Rails generates a seed and uses it to randomly order the tests. If you want to re-run the tests in the same order, you can pass a seed value to the test command and Rails will use that as the seed:

~$ bin/rails test TESTOPTS=--seed 44646

Tests are always run in the same order for a given seed. But how does that work? Let’s dig through the call stack to find where the test ordering happens.

Under the hood, Rails uses the Minitest gem as the test framework. The overall call stack looks like this:

~$ bin/rails test
  Rails::TestUnit::Runner.rake_run
    Rails::TestUnit::Runner.run
      Minitest.autorun
        Minitest.run(args)

Within the Minitest::run definition, the command line arguments from the rake task invocation get processed:

desc = "Sets random seed. Also via env. Eg: SEED=n rake"
opts.on "-s", "--seed SEED", Integer, desc do |m|
  options[:seed] = m.to_i
end

A bit later, options[:seed] is passed to a function I’ve never seen before, srand:

srand options[:seed]

Here’s what the documentation says about that method:

Seeds the system pseudo-random number generator, Random::DEFAULT, with number. The previous seed value is returned.

If number is omitted, seeds the generator using a source of entropy provided by the operating system, if available (/dev/urandom on Unix systems or the RSA cryptographic provider on Windows), which is then combined with the time, the process id, and a sequence number.

srand may be used to ensure repeatable sequences of pseudo-random numbers between different runs of the program. By setting the seed to a known value, programs can be made deterministic during testing.

That last paragraph is key: We can make random things deterministic by setting the seed to an arbitrary value. Let’s dig just a little further and take a look at how Minitest is randomizing test orders. That appears to happen here, where we call shuffle on the array of runnable tests. Let’s try that method in an IRB session along with some srand calls to test the behavior:

>> array = %w(Lorem ipsum dolor sit amet consectetur)
=> ["Lorem", "ipsum", "dolor", "sit", "amet", "consectetur"]

>> array.shuffle
=> ["consectetur", "Lorem", "sit", "amet", "dolor", "ipsum"]
>> array.shuffle
=> ["Lorem", "ipsum", "dolor", "consectetur", "sit", "amet"]

>> srand 42
=> 42
>> array.shuffle
=> ["Lorem", "ipsum", "consectetur", "dolor", "amet", "sit"]
>> array.shuffle
=> ["sit", "Lorem", "ipsum", "dolor", "consectetur", "amet"]

>> srand 42
=> 42
>> array.shuffle
=> ["Lorem", "ipsum", "consectetur", "dolor", "amet", "sit"]
>> array.shuffle
=> ["sit", "Lorem", "ipsum", "dolor", "consectetur", "amet"]

As expected, Array#shuffle randomizes the order. If we call srand with a specific value, we can get repeatable orders out of a function meant to randomize the array items.

If my previous post about the PHP Internals inspired you to dig a bit deeper, you can check out the C implementation of the Array shuffle methods here:

Thanks for reading!

If you have any comments or feedback on this article, I’d love to hear from you. The best way to reach me is on Twitter.