Cleaner RSpec Controller Specs

Testing is an important part of software development. Having a solid test suite allows you develop more confidently – enable you to make large refactorings without fear of unintended consequences. I test almost all the code I write.

Recently, I found a bug that allowed users to see ‘inactive’ products; so I wrote a test to prevent that. I’ve seen lots of specs that test active products like so:

1
2
3
4
5
6
7
8
9
it "scopes products to active and available" do
    not_active = Factory(:product, :active => false)
    not_available = Factory(:product, :available => false)
    active_available = Factory(:product, :active => true, :available => true)
    get :index
    assigns(:products).should_not include(not_active)
    assigns(:products).should_not include(not_available)
    assigns(:products).should include(active_available)
end

Test like this are wordy and can lead to a ton of factoried objects. There’s got to be a better way!!

1
2
3
4
5
it "scopes products to active and available" do
    get :index
    assigns(:products).should have_scope(:active)
    assigns(:products).should have_scope(:available)
end

Ahhh how refreshing… See that have_scope method there? That matcher is accomplished by applying the scope to the set of products and asserting that it should be the same ActiveRecord::Relation. In most cases:

  ActiveRecord::Relation + scope == ActiveRecord::Relation + scope + scope

so you can assert that your products relation is unchanged by adding the desired scope. This isn’t always the case though. Some orders and wheres just keep getting tacked onto the end of the relation… so it falls back to an array comparison, and that is unawesome. Anywho, the final product is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RSpec::Matchers.define :have_scope do |scope_name, *args|                                                                                             
  match do |actual|
    actual.send(scope_name, *args) == actual
  end

  failure_message_for_should do |actual|
    "Expected relation to have scope #{scope_name} #{args.present? ? "with args #{args.inspect}" : ""} but it didn't " + actual.to_sql
  end

  failure_message_for_should_not do |actual|
    "Expected relation not to have scope #{scope_name} #{args.present? ? "with args #{args.inspect}" : ""} but it didn't " + actual.to_sql
  end

  description do
    "have scope #{scope_name} #{args.present? ? "with args #{args.inspect}" : ""}"
  end
end

This has helped me write legible and concise specs, mileage my vary.