Created
July 30, 2024 19:28
-
-
Save jaydorsey/355cba31edb41dc2e3b21e7c4dcff79b to your computer and use it in GitHub Desktop.
How to write flakey specs with rails, rspec, factory_bot, and faker
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# frozen_string_literal: true | |
# 1. Put this in your spec folder as spec/flakey_spec.rb | |
# 2. Run it with `rspec spec/flakey_spec.rb` | |
# | |
# Assumptions: | |
# | |
# 1. You're using rails, rspec, factory_bot, faker | |
# 2. You have a standard-ish User model (has create_at, updated_at, etc. integer ID primary key, etc) | |
# 3. User has a last_name field/column/attribute | |
# 4. You have a factory something like this: | |
# | |
# factory :user do | |
# first_name { Faker::Name.first_name } | |
# last_name { Faker::Name.last_name } | |
# end | |
RSpec.describe 'How to write a flakey spec' do | |
describe 'freeze order matters' do | |
# More commonly, you'll have a let(:user) (no !), and then reference that via another let that gets loaded | |
# before the freeze_time gets called. Most of the time, your spec will pass, but sometimes it will definitely | |
# fail | |
let!(:user) { create(:user) } # this gets created first | |
context 'when we freeze after we let/let!' do | |
before { freeze_time } # then this runs | |
let(:time) { Time.zone.now.to_i } # then this | |
it 'passes most of the time' do | |
expect(user.created_at.to_i).to eq(time) # then our expectation is run | |
end | |
end | |
end | |
describe 'postgres is non-deterministic unless you specify the order' do | |
# These records should always be created with sequential incremental IDs, but User.all will return the | |
# records non-deterministically. The solution is to add at least 1 order clause, preferable 2 if one of the | |
# order values isn't guaranteed to be unique | |
# | |
# e.g. | |
# | |
# order(last_name: :asc) -> Not the best, because we know people share last names | |
# order(last_name: :asc, id: :asc) -> Better, because id's are unique and a deterministic tiebreaker | |
# | |
# Straight from postgres docs https://www.postgresql.org/docs/current/queries-order.html | |
# | |
# > If sorting is not chosen, the rows will be returned in an unspecified order | |
let!(:user_1) { create(:user) } | |
let!(:user_2) { create(:user) } | |
it 'passes most of the time' do | |
expect(User.all.pluck(:id)).to eq([user_1.id, user_2.id]) | |
end | |
it 'passes all of the time' do | |
expect(User.all.pluck(:id)).to contain_exactly(user_1.id, user_2.id) | |
end | |
end | |
describe 'faker data can sometimes collide' do | |
let!(:user_1) { create(:user, last_name: 'Smith') } | |
# Faker will sometimes create this record with a last name of `Smith`. A better solution is to be explicit | |
# about the conditions of your test and not leave it up to Faker: | |
# | |
# let!(:user_2) { create(:user, last_name: 'Jones') } | |
# | |
# Another option is to use Faker::Names.unique.last_name in the factory which makes faker try it's best to | |
# not re-use values | |
let!(:user_2) { create(:user) } | |
it 'passes almost all of the time' do | |
expect(User.where(last_name: 'Smith').count).to eq(1) | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment