Reputation: 409
In section 6.2.4 of Ruby on Rails 3 Tutorial, Michael Hartl describes a caveat about checking uniqueness for email addresses: If two identical requests come close in time, request A can pass validation, then B pass validation, then A get saved, then B get saved, and you get two records with the same value. Each was valid at the time it was checked.
My question is not about the solution (put a unique constraint on the database so B's save won't work). It's about writing a test to prove the solution works. I tried writing my own, but whatever I came up with only turned out to mimic the regular, simple uniqueness tests.
Being completely new to rspec, my naive approach was to just write the scenario:
it 'should reject duplicate email addresses with caveat' do
A = User.new( @attr )
A.should be_valid # always valid
B = User.new( @attr )
B.should be_valid # always valid, as expected
A.save.should == true # save always works fine
B.save.should == false # this is the problem case
# B.should_not be_valid # ...same results as "save.should"
end
but this test passes/fails in exactly the same cases as the regular uniqueness test; the B.save.should == false
passes when my code is written so that the regular uniqueness test passes and fails when the regular test fails.
So my question is "how can I write an rspec test that will verify I'm solving this problem?" If the answer turns out to be "it's complicated", is there a different Rails testing framework I should look at?
Upvotes: 4
Views: 763
Reputation: 434645
It's complicated. Race conditions are so nasty precisely because they are so difficult to reproduce. Internally, save
goes something like this:
So, to reproduce the timing problem, you'd need to arrange the two save
calls to overlap like this (pseudo-Rails):
a.validate # first half of a.save
b.validate # first half of b.save
a.write_to_db # second half of a.save
b.write_to_db # second half of b.save
but you can't open up the save
method and fiddle with its internals quite so easily.
But (and this is a big but), you can skip the validations entirely:
Note that
save
also has the ability to skip validations if passed:validate => false
as argument. This technique should be used with caution.
So if you use
b.save(:validate => false)
you should get just the "write to database" half of b
's save
and send your data to the database without validation. That should trigger a constraint violation in the database and I'm pretty sure that will raise an ActiveRecord::StatementInvalid exception so I think you'll need to look for an exception rather than just a false return from save
:
b.save(:validate => false).should raise_exception(ActiveRecord::StatementInvalid)
You can tighten that up to look for the specific exception message as well. I don't have anything handy to test this test with so try it out in the Rails console and adjust your spec appropriately.
Upvotes: 4