Cleaner RSpec Controller Specs
RSpec, dev, ruby, testingTesting 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 + scopeso 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.