Two Shoulda best practices
In praise of Shoulda macros
Shoulda contexts let you to share setup code between different tests. This is for me one of Shoulda’s most attractive features.
When you combine this with the technique of defining your own macros to encapsulate assertions or setups that come up often, you end up with seriously DRY and readable tests.
I see a few different kinds of Shoulda macros:
Assertion macros
Assertions macros often begin with should_. They encapsulate one or a few assertions.
For ActiveRecord models:
should_require_attributes :name, :phone_number
They may even accept a block and do assertions on its execution, like should_raise:
should_raise(LoadError, :message => /vespene/) do
require "more vespene gas"
end
To learn more about assertion macros, you can take a look at Shoulda macros allows you to embrace your inner slacker by Josh Nichols. Inner slacker? I’m right there!
Setup macros
This kind of macro encapsulates a setup that comes up often in your test suite. One inspired by Restful Authentication’s login_as helper method could be used like this:
logged_in_as :mat do
# Shoulda tests
end
These kinds of macros accept a block that defines more Shoulda tests, rather than a block of code testing your app per se.
Turnkey macros
Turnkey macros are beefed up assertion macros. The main difference is their extent. They contain many contexts and a lot of should blocks. They usually accept substantial options hashes or are configured with a setup block. Like should_be_restful in the following example, inspired by the Shoulda documentation:
logged_in_as :stranger do
should_be_restful do |resource|
resource.create.params = { :subject => "test", :body => "message" }
resource.denied.actions = [:edit, :update, :destroy]
resource.denied.redirect = "login_url"
resource.denied.flash = /only the owner can/i
end
end
Two Shoulda best practices around setup macros
This article is specifically about setup macros.
Here’s the implementation of a pretty generic Shoulda macro I could define in my test_helper*. This is an implementation of the macro I mentioned at the beginning:
# Sets the current person in the session from the person fixtures.
def self.logged_in_as(person, &block)
context "logged in as #{person}" do
setup do
@request.session[:person] = people(person).id
end
yield
end
end
Which can then be used like this in any controller test:
logged_in_as :mat do
# tests for users
end
logged_in_as :admin do
# tests for admin
end
Setup macros have a very subtle catch, however. Here’s a modified version of the first example above:
logged_in_as :mat do
setup do
@request.session[:last_login] = Time.now
end
# Some tests
end
The setup block you see here is never going to be executed. Why?
If we were to replace the logged_in_as macro by the actual code it contains, here’s what it would look like:
context "logged in as #{person}" do
setup do
@request.session[:person] = people(person).id
end
setup do
@request.session[:last_login] = Time.now
end
# Some tests
end
Does that make sense? Not so sure.
Shoulda doesn’t like to have multiple setup blocks for a given context. That part does make sense.
Best practice #1: Always describe the situation with a context.
You should always describe the situation in which your test takes place (what your setup is doing) with a context.
logged_in_as :mat do
context "with last login set to now" do
setup do
@request.session[:last_login] = Time.now
end
# Some tests
end
end
Fair enough. We blame it on the user of the macro :-)
Since we’re using Ruby, most of us are probably in agreement with Matz’ “Make the programmer happy” motto.
So can we also solve the problem from the other end? Create a setup macro that supports a direct inner setup block? Of course we can, this is Ruby, not VB.
Best practice #2: Create setup macros that support a second setup block
A setup is grafted to a context that describes it. As the creator of the macro, I don’t know what crazy setup blocks programmers will put inside their macro. So I simply create a mute context:
# Sets the current person in the session from the person fixtures.
def self.logged_in_as(person, &block)
context "logged in as #{person}" do
setup do
@request.session[:person] = people(person).id
end
context '' do
yield
end
end
end
Now my macro supports the following test without a hitch:
logged_in_as :mat do
setup do
@request.session[:last_login] = Time.now
end
# Some tests
end
And of course, programmers who stick to best practice #1 can still write a cleaner test without a problem. The awesomeness of contexts lies in the fact that they can be nested:
logged_in_as :mat do
context "with last login set to now" do
setup do
@request.session[:last_login] = Time.now
end
# Some tests
end
end
Conclusion
Best practice #1 is simple. A setup block should be described by its encompassing context. It’s a question of readability. Nesting a setup block immediately inside a Shoulda macro is a dubious practice.
Best practice #2 is a more pragmatic solution to the problem. Ok, nesting a block right inside a macro isn’t always the best idea.
But when you don’t have the macro right under your nose, it may take you a while before you think about looking at said macro. I don’t know about you, but I have a tendency to have a great deal of confidence in macros that work well across my test suite.
So after you’ve spent an hour questioning Shoulda (or your sanity, or whether you should have become a gardener instead of a software developer) because your setup block isn’t executing, best practice #2 starts to make sense.
It may or may not be necessary in all your setup macros. I find it’s especially useful for macros that are generic enough to be used across your test suite. Or most of all, in setup macros you will share with the world.
Best practice #2 makes setup macros bulletproof to the problem of multiple setups.
Now go refactor your setup macros!
To learn more about Shoulda, check out Thoughtbot’s comprehensive documentation.
* A note on where to define Shoulda macros. Shoulda 2 can now auto load macros that are in the right location. This will help you keep your test_helper cleaner. Read more about it succinctly in Shoulda can automatically load custom macros by Josh Nichols or in the Shoulda 2.0 release post.