Michael MacDonald

Archive for 2012|Yearly archive page

Testing Scopes with Lambdas

In Rails, Testing on February 24, 2012 at 3:50 pm

How do you know that your scopes are correct? At work we had been plagued by mysterious multiple payments in one of our apps. It had been coded to prevent multiple payments yet some customers were somehow managing to do it. After a thorough investigation, I discovered that the problem simply came down to one of the scopes used in the code had been written incorrectly. Here it is:

scope :current, where('start_date <= ?', Date.today).where('end_date > ?', Date.today)

Do you see the problem? It is a scope that checks the object is current, that it has started but not yet ended. Unfortunately, this scope is missing a lambda.

scope :current, lambda { where('start_date <= ?', Date.today).where('end_date > ?', Date.today) }

Without the lambda, the scope is set to the date when the scope was first loaded so that Date.today remain static until the next deploy or when the production app is restarted. So it’ll work fine for a day and then it’ll start misbehaving. The lambda effectively defer the evaluation of the expression until it is needed. So each time you use Authorisation.current it’ll use the current date which is what you want.

So how do we avoid this mistake? Here’s the wrong way to test a scope that uses a lambda:

expired_auth = Authorisation.create(start_date: 4.days.ago, end_date: 2.days.ago)
current_auth = Authorisation.create(start_date: 2.days.ago, end_date: 2.days.from_now)

Authorisation.current.should include(current_auth)
Authorisation.current.should_not include(expired_auth)

This will pass the original non-lambda scope. It passes because it’s not really testing the dynamic nature of the scope provided by the lambda. For this I use the Timecop gem:

auth = Authorisation.create(start_date: 2.days.ago, end_date: 2.days.from_now) # or use a factory to give you a current auth
Authorisation.current.should include(auth)

Timecop.freeze(2.days) # fast forward by 2 days so that the current auth is now by definition no longer current
Authorisation.current.should_not include(auth)

Now this test fails with the non-lambda scope. When I change the scope to use lambda, the test now passes.