Testing Named Scopes
Last year Zach wrote a blog post about an easy way to test ActiveRecord's
named_scope. It was one of the first snippets he brought with him to the code base here at Square. We've been using it extensively ever since. Recently, we made some improvements to the test helper for clarity and ease of use, so we decided it was time to share them with you.
The basic idea is to make it easy to test that your
named_scope returns the correct, expected subset of your items. We declare our test condition in Ruby such that it is true for everything in our subset and false for everything outside it. This lets us express the intent of our
named_scope in succinct Ruby code allowing us to easily test drive our SQL.
Let's take a
Payment class that has an
amount_cents field and the normal Rails
created_at timestamp. We'll write a test using our helper for a
large_payments_in_last_12_hours that—you guessed it—returns all payments from the last 12 hours over $100. The diagram below expresses the intent visually: our test will have a condition expressed in Ruby that is true for everything in the blue area and false for everything in the green area.
should_be_a_subset helper integrates with RSpec, so the example below uses RSpec syntax and depends on rspec-rails. The condition that describes
large_payments_in_last_12_hours asserts that each payment in the subset was created over 12 hours ago and that it has an
amount_cents greater than
Now that we have a test to drive it, we can write the implementation:
This example shows how to test a simple SQL-based
named_scope with a short Ruby expression. The helper really proves its worth with more complicated named scopes, especially those with multiple
joins or sub-
Let's take a closer look at how
should_be_a_subset works. Here's the helper's code so you can follow along:
One of the first things it does is make sure that you've actually created objects that will fall both inside and outside your subset. This double-check is to ensure that you're really testing the intended scoping behavior by helping to prevent false passes due to insufficient data. We make sure to flunk the test with nice error messages when you haven't set up your pre-conditions correctly so you can fix it and move along instead of chasing down hard-to-diagnose errors due to fixture data or faulty assumptions.
The assertions in the helper then test that our constrained set (here the
named_scope you're testing) matches the objects for which your ruby condition returns true. It also checks that the excluded set (superset - constrained set) returns false for the condition you provided. We then map over the ids of the objects for our test condition instead of the objects themselves so error messages are easier to read.
The code is pretty simple, but everyone we've shown it to has loved it. It's made expressing the intention of
named_scopes really simple and helped us to design and test them more thoroughly. We hope it will help you, too.