mcdave
mcdave

Reputation: 409

Railstutorial.org Validating unique email

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

Answers (1)

mu is too short
mu is too short

Reputation: 434645

It's complicated. Race conditions are so nasty precisely because they are so difficult to reproduce. Internally, save goes something like this:

  1. Validate.
  2. Write to database.

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

Related Questions