Comparing times in Ruby and Rails testsEdit
The pitfall
One thing I've found when writing specs for Active Record models is that simple timestamp comparisons can often fail in a surprising ways. The reason for this is as follows:
post = Post.create # save a post to the database
other = Post.last # load the post back from the database
# some surprising results
other == post # => true
other.updated_at == post.updated_at # => false
other.updated_at.to_s == post.updated_at.to_s # => true
other.updated_at.to_f == post.updated_at.to_f # => false
# so why is this happening?
post.updated_at # => Fri, 04 Feb 2011 02:13:57 UTC +00:00
other.updated_at # => Fri, 04 Feb 2011 02:13:57 UTC +00:00
post.updated_at.to_f # => 1296785637.88479
other.updated_at.to_f # => 1296785637.0
As you can see, when we first write to the database our in-memory copy of the record has a non-integral timestamp corresponding to the exact value of Time.now at the time the record was written, including microseconds.
When we do a post.reload or otherwise read the data back (via Post.find or Post.last etc), we see that the actual timestamp in the database is rounded off to a whole number of seconds.
As a result, even though the two record instances really refer to the same record in the database and the updated_at attribute should therefore be the same, the actual values are non-identical because one of them is rounded off during the round-trip through the database while the other retains its original, exact value.
There are a zillion different ways in which this kind of thing can spring up in a spec suite and I won't bother listing them here, but you'll recognize the problem when you see it because two seemingly equivalent dates, a and b, when compared using a.should == b or similar will cause your specs to fail.
Workarounds
Compare strings rather than dates
model.updated_at.to_s.should == other.updated_at.to_s
Use the be_within/of matcher
model.updated_at.should be_within(1).of(other.updated_at)
This works because the be_within matcher does a subtraction of the two Time instances, which returns a float.
Reload
This one is a little unseemly. With this technique, when you create a model, you perform an immediate model.reload in order to get the round-trip out the way before you try any comparisons.