Using macros to create custom example groups in RSpec
I am involved in a personal project where I am developing a web application with Rails. I am using RSpec and Cucumber, and I am following an outside-in, behavior-driven development approach, as recommended in the excellent RSpec Book. In this post, I would like to share a problem I found and how I solved it.
It started when I tried to refactor some duplicated code in my controller’s specs. It was related to the authentication system. Specs should test their behavior in both authenticated and insecure contexts, to be sure that anything happens if an anonymous user sends a POST
action to your WorldDestroyerController
. For example:
describe UserSessionsController, "DELETE destroy" do
should_require_login :delete, :destroy
context "authenticated user" do
before(:each) do
login_as_user
end
it "should destroy the user session" do
@current_user_session.should_receive(:destroy)
delete :destroy
end
it "should redirect to the login page" do
delete :destroy
response.should redirect_to login_path
end
end
end
The next fragment was recurrent in all my examples.
context "authenticated user" do
before(:each) do
login_as_user
end
For every example in the example group, it basically stubs the controller method that validates sessions in order to get a valid one when invoked (I am using authlogic). I wanted to create a macro context_authenticated
so I could code the previous example like this:
describe UserSessionsController, "DELETE destroy" do
should_require_login :delete, :destroy
context_authenticated do
it "should destroy the user session" do
@current_user_session.should_receive(:destroy)
delete :destroy
end
it "should redirect to the login page" do
delete :destroy
response.should redirect_to login_path
end
end
end
My first attempt was to write a macro that included the before the block and yielded to the block provided in the invocation:
def context_authenticated
context "authenticated user" do
before(:each) do
login_as_user
end
yield #Wrong
end
end
The examples failed because the before(:each)
fragment was not being invoked before executing them. To discover the problem I had to investigate a little bit about how the RSpec DSL works. Basically:
- Each
context
(alias fordescribe
) creates a newExampleGroup
subclass. - Each
it
(alias forexample
) creates a new instance of the currentExampleGroup
subclass where it is invoked.
So the problem was related to the very nature of closures. In Ruby, a block of code (proc
or lambda
) maintains the bindings in effect for that closure. The bindings contain not only variable references but also the self
reference itself.
In my case, what I was doing was to submit a closure with the it
examples to the macro. The execution context of this closure was the one where it was defined: the ExampleGroup
subclass created by describe UserSessionsController...
. However, inside the macro, a new ExampleGroup
subclass was created, and the before
block was associated with that example group. That was the reason why the the before
code was not getting executed before the submitted examples. Once I understood this, the solution was simple:
def context_authenticated(&example_group_block)
example_group_class = context "authenticated user" do
before(:each) do
login_as_user
end
end
example_group_class.class_eval &example_group_block
end
What the previous code does is to change the evaluation context of the closure, so it gets executed inside the ExampleGroup
class created by the macro.