About This Article
Have you ever experienced tests that occasionally fail when running your RSpec test suite on CI? These are what we call flaky tests.
Simply re-running the test until it passes and calling it a day is a missed opportunity. Let’s take a more sustainable approach to fixing them.
Enable config.order
and Kernel.srand
The key idea here is simple: enable the following configuration in your RSpec settings file, which is generated by default but commented out.
# spec/spec_helper.rb
# Run specs in random order to surface order dependencies. If you find an
# order dependency and want to debug it, you can fix the order by providing
# the seed, which is printed after each run.
# --seed 1234
config.order = :random
# Seed global randomization in this process using the `--seed` CLI option.
# Setting this allows you to use `--seed` to deterministically reproduce
# test failures related to randomization by passing the same `--seed` value
# as the one that triggered the failure.
Kernel.srand config.seed
=end
Once enabled, this lets you reproduce random test failures deterministically using the seed printed after each test run.
These lines are typically wrapped in =begin
and =end
comments when generated, so make sure to move them out to enable them.
=begin
These lines
# are all comments
=end
A Hands-On Example of Dealing with Flaky Tests
Let’s walk through an example to see how flaky tests can be identified and fixed.
Setup from rails new
# Create a new Rails app
$ rails new --skip-test rspec-rails-test
$ cd rspec-rails-test/
# Add and set up rspec-rails
$ bundle add rspec-rails --group development,test
$ rails generate rspec:install
Now activate the config.order
and Kernel.srand
settings by moving them outside of the comment block.
config.profile_examples = 10
+=end
config.order = :random
Kernel.srand config.seed
-=end
end
Confirm RSpec runs successfully with zero examples:
$ bundle exec rspec
No examples found.
Randomized with seed 37597
Create a Model and Write a Test
Let’s create a simple model named YearMonth
with year
and month
attributes, and some basic validations.
# app/models/year_month.rb
class YearMonth
include ActiveModel::Model
include ActiveModel::Attributes
attribute :year, :integer
attribute :month, :integer
validates :year, numericality: { only_integer: true }
validates :month, numericality: { only_integer: true, in: (1..12) }
end
Now, add a basic test to confirm that the model is valid.
# spec/models/year_month_spec.rb
require "rails_helper"
RSpec.describe YearMonth do
subject { YearMonth.new(attributes) }
let(:attributes) { { year:, month: } }
let(:year) { rand(2100) }
let(:month) { rand(12) }
it do
expect(subject.valid?).to be_truthy
end
end
Run the Test
$ bundle exec rspec
Randomized with seed 54242
.
Finished in 0.0191 seconds
1 example, 0 failures
If you were unlucky and saw a failure like this, just re-run the test and confirm it eventually passes:
Randomized with seed 30925
F
Failures:
1) YearMonth is expected to be truthy
Failure/Error: expect(subject.valid?).to be_truthy
expected: truthy value
got: false
Observe Occasional Failures
Actually, this test sometimes fails.
After several runs, you should see an abnormal termination as shown below.
It is like a CI that is terminating normally and cheerfully in the product code, but sometimes notifies you of a failure.
$ bundle exec rspec
Randomized with seed 30925
F
Failures:
1) YearMonth is expected to be truthy
Failure/Error: expect(subject.valid?).to be_truthy
expected: truthy value
got: false
# ./spec/models/year_month_spec.rb:11:in 'block (2 levels) in <top (required)>'
Finished in 0.01313 seconds (files took 0.50544 seconds to load)
1 example, 1 failure
Failed examples:
rspec ./spec/models/year_month_spec.rb:10 # YearMonth is expected to be truthy
Randomized with seed 30925
$
In real-world projects, it's useful to automate the test run in a loop and go grab lunch. Here’s a sample shell script:
#!/bin/bash
set -euxo pipefail
i=0
while true
do
i=$((i + 1))
bundle exec rspec
done
Reproduce the Failure
The key detail is this line in the output:
Randomized with seed 30925
You can reproduce the exact test order and failure using this seed:
$ bundle exec rspec --seed 30925
Debug the Failure
Now that it’s reproducible, debugging is straightforward. Add a puts
statement to inspect the attributes:
it do
+ puts "attributes: #{subject.attributes.inspect}"
expect(subject.valid?).to be_truthy
end
$ bundle exec rspec --seed 30925
attributes: {"year" => 1913, "month" => 0}
F
The month
is 0
— that’s not valid. The issue is with rand(12)
, which generates values from 0
to 11
.
let(:month) { rand(12) }
> 10000.times.map { rand(12) }.uniq.sort
=> [0, 1, ..., 11]
Fix the Root Cause
Fix the range to 1..12
instead of 0..11
:
- let(:month) { rand(12) }
+ let(:month) { rand(1..12) }
> 10000.times.map { rand(1..12) }.uniq.sort
=> [1, 2, ..., 12]
Now commit and push your fix:
git commit -m 'fix flaky test'
Summary
- Enabling
config.order = :random
andKernel.srand config.seed
inspec/spec_helper.rb
helps identify and debug flaky tests. - Use the printed seed value and
--seed
option to reliably reproduce failures. - Don’t leave flaky tests lying around — fix them to ensure your CI surfaces real issues, not noise.
Top comments (0)