Testing Named Scopes

October 14, 2010

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.

Example

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 named_scope called 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.

The Test

The 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 100_00:

The Implementation

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-selects.

The Helper

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.

Zach Brock
Server team engineer. Mostly made of water. @z.
Cameron Walters
Server team engineer. @ceedub.

Comments

Get support help at squareup.com/support. We'll delete off-topic comments.