Skip to content

Instantly share code, notes, and snippets.

@jaydorsey
Created July 30, 2024 19:28
Show Gist options
  • Save jaydorsey/355cba31edb41dc2e3b21e7c4dcff79b to your computer and use it in GitHub Desktop.
Save jaydorsey/355cba31edb41dc2e3b21e7c4dcff79b to your computer and use it in GitHub Desktop.
How to write flakey specs with rails, rspec, factory_bot, and faker
# 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