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:
describe) creates a new
example) creates a new instance of the current
ExampleGroupsubclass where it is invoked.
So the problem was related to the very nature of closures. In Ruby, a block of code (
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.