Nick Sieger: Customizing RSpectag:blog.nicksieger.com,2005:TypoTypo2007-08-31T16:45:16+00:00Martinurn:uuid:dc63ac2a-ed7f-4e2e-b72e-a3216c25fe792007-03-06T18:07:48+00:002007-08-31T16:45:16+00:00Comment on Customizing RSpec by Martin<p>Great post - thanks Nick.</p>Michal Kwiatkowskiurn:uuid:8d162cc9-12ae-44e7-8154-84ea0cfe4a972007-01-19T20:40:20+00:002007-08-31T16:45:16+00:00Comment on Customizing RSpec by Michal Kwiatkowski<p>I’m using RSpec in my svntl project (<a href='http://code.google.com/p/svntl/' rel="nofollow">http://code.google.com/p/svntl/</a>) and had the same need to extend my contexts with custom methods. All I’ve done though was extending module Spec::Runner::ContextEval::ModuleMethods with methods (as I’ve described in this snippet: <a href='http://www.bigbold.com/snippets/posts/show/3210' rel="nofollow">http://www.bigbold.com/snippets/posts/show/3210</a>).</p>
<p>Your recipe solves the general problem, but most of the time you won’t need anything more than this simple straightfoward solution.</p>Nick Siegerurn:uuid:21755a2d-0ffa-4f0c-be7d-a2e2e01d99b42007-01-02T05:32:00+00:002007-08-31T16:45:13+00:00Customizing RSpec<p><em>Update/Disclaimer: I refer to parts of RSpec that are not blessed as an extension API. Redefining <code>before_context_eval</code> and using the <code>@context_eval_module</code> variable directly may change in the future. I’ll keep this article updated to coincide with the changes. For now, these techniques should work fine with RSpec versions up to 0.7.5.</em></p>
<p>RSpec seems to be getting more attention lately as a viable, nay, <em>preferred</em>, alternative to Test::Unit. It’s possible that it’s just my personal feed-reader-echo-chamber, but consider this: <a href="http://blog.fallingsnow.net/rubinius/">Rubinius</a> has started using RSpec alongside Test::Unit as an another way to test the alternate Ruby implementation. They’re even in the midst of building some snazzy extensions to allow the same specs to be run under a Ruby implementation of your choice. (Perhaps this will point the way to a new round of executable specs to accompany the <a href="http://www.headius.com/rubyspec/index.php/Main_Page">fledgling community spec</a>? Let’s wait and see how they do and leave that topic for another day.)</p>
<p>But extending and customizing RSpec to add a DSL on top of RSpec’s <code>context/specify</code> framework doesn’t have to be the realm of experts. Here are some templates for how you can DRY up your specs by adding your own helper methods in such a way that they will be available to all your specs. But first, a little background.</p>
<h3>Spec Helper</h3>
<p>Most usages of RSpec that I’ve seen in the wild use a “spec helper” (<code>spec_helper.rb</code>). This file, following the pattern of Rails’ <code>test_helper.rb</code>, minimally contains require statements to pull in the RSpec code and any supporting code for running specs. By requiring the spec helper via a path relative to your spec (usually with <code>require File.dirname(__FILE__) + '/spec_helper'</code> or similar), it also allows you the convenience of running your specs one at a time from anywhere (say, by launching from your editor) or with <code>rake</code> or <code>spec</code>. This file is where your shared helper methods will go, and where they’ll get registered to be pulled into the contexts.</p>
<h3>What Context in <code>context</code>?</h3>
<div class="typocode"><pre><code class="typocode_ruby "><span class="ident">context</span> <span class="punct">"</span><span class="string">A new stack</span><span class="punct">"</span> <span class="keyword">do</span>
<span class="comment"># <== What is the value of "self" here?</span>
<span class="ident">specify</span> <span class="punct">"</span><span class="string">should be empty</span><span class="punct">"</span> <span class="keyword">do</span>
<span class="keyword">end</span>
<span class="keyword">end</span></code></pre></div>
<p>How do those contexts work anyway? The <code>context</code> method that defines a context in which specs can be defined and run takes a block to define the individual specs, but what can really go in that block?</p>
<p>It turns out that RSpec jumps through metaprogramming hoops (using <code>class_eval</code>) to make the block behave like a class definition. This means you can do things like put method definitions inside your context:</p>
<div class="typocode"><pre><code class="typocode_ruby "><span class="ident">context</span> <span class="punct">"</span><span class="string">A new stack</span><span class="punct">"</span> <span class="keyword">do</span>
<span class="keyword">def </span><span class="method">a_new_stack</span>
<span class="constant">Stack</span><span class="punct">.</span><span class="ident">new</span>
<span class="keyword">end</span>
<span class="ident">specify</span> <span class="punct">"</span><span class="string">should be empty</span><span class="punct">"</span> <span class="keyword">do</span>
<span class="ident">a_new_stack</span><span class="punct">.</span><span class="ident">should_be_empty</span>
<span class="keyword">end</span>
<span class="keyword">end</span></code></pre></div>
<p>Which is nice, but the reason we’re here is to hide that away in <code>spec_helper.rb</code>. So, to get back to the point of the comment in the first example above, the <code>self</code> inside the context block is an anonymous <code>Module</code> object. It’s constructed in the <code>initialize</code> method of a <code>Context</code> (condensed from <code>spec/runner/context.rb</code> in the RSpec codebase):</p>
<div class="typocode"><pre><code class="typocode_ruby "><span class="keyword">class </span><span class="class">Spec::Runner::Context</span>
<span class="keyword">def </span><span class="method">initialize</span><span class="punct">(</span><span class="ident">name</span><span class="punct">,</span> <span class="punct">&</span><span class="ident">context_block</span><span class="punct">)</span>
<span class="attribute">@name</span> <span class="punct">=</span> <span class="ident">name</span>
<span class="attribute">@context_eval_module</span> <span class="punct">=</span> <span class="constant">Module</span><span class="punct">.</span><span class="ident">new</span>
<span class="attribute">@context_eval_module</span><span class="punct">.</span><span class="ident">extend</span> <span class="constant">ContextEval</span><span class="punct">::</span><span class="constant">ModuleMethods</span>
<span class="attribute">@context_eval_module</span><span class="punct">.</span><span class="ident">include</span> <span class="constant">ContextEval</span><span class="punct">::</span><span class="constant">InstanceMethods</span>
<span class="ident">before_context_eval</span>
<span class="attribute">@context_eval_module</span><span class="punct">.</span><span class="ident">class_eval</span><span class="punct">(&</span><span class="ident">context_block</span><span class="punct">)</span>
<span class="keyword">end</span>
<span class="keyword">def </span><span class="method">before_context_eval</span>
<span class="keyword">end</span>
<span class="keyword">end</span></code></pre></div>
<p>(Take note of that empty <code>before_context_eval</code> method and the fact that it’s invoked during context initialization; that’s where we can plug in our custom extensions.)</p>
<p>The object held by the <code>@context_eval_module</code> instance variable is being augmented in two ways: extension and inclusion. The object is <em>extended</em> with the <code>ContextEval::ModuleMethods</code> module; these methods are being added to the object’s singleton class. This has the effect of making these methods visible within the <code>context</code> block, functioning similar to “class” methods.</p>
<p>The object also has the <code>ContextEval::InstanceMethods</code> module <em>included</em>. This has the effect of adding these as instance methods, making them visible from within <code>specify</code> blocks, which are made to behave like instance methods on the same object.</p>
<h3>Putting it together</h3>
<table style="text-align: left;" summary="">
<thead>
<th>Technique</th>
<th>Visibility</th>
<th>Use</th>
</thead>
<tbody>
<tr><td>@context_eval_module.extend</td><td>Context block</td><td>Custom setup, shared state declaration</td></tr>
<tr><td>@context_eval_module.include</td><td>Specify block</td><td>Shared actions/functions, stub/expectation modification, encapsulate instance variables</td></tr>
</tbody>
</table>
<h4>Adding specialized setup methods</h4>
<p><code>spec_helper.rb</code> snippet:</p>
<div class="typocode"><pre><code class="typocode_ruby "><span class="keyword">module </span><span class="module">SharedSetupMethods</span>
<span class="keyword">def </span><span class="method">setup_new_stack</span>
<span class="ident">setup</span> <span class="keyword">do</span>
<span class="attribute">@stack</span> <span class="punct">=</span> <span class="constant">Stack</span><span class="punct">.</span><span class="ident">new</span>
<span class="keyword">end</span>
<span class="keyword">end</span>
<span class="keyword">end</span>
<span class="keyword">class </span><span class="class">Spec::Runner::Context</span>
<span class="keyword">def </span><span class="method">before_context_eval</span>
<span class="attribute">@context_eval_module</span><span class="punct">.</span><span class="ident">extend</span> <span class="constant">SharedSetupMethods</span>
<span class="keyword">end</span>
<span class="keyword">end</span></code></pre></div>
<p>Example spec:</p>
<div class="typocode"><pre><code class="typocode_ruby "><span class="ident">context</span> <span class="punct">"</span><span class="string">A new stack</span><span class="punct">"</span> <span class="keyword">do</span>
<span class="ident">setup_new_stack</span>
<span class="ident">specify</span> <span class="punct">"</span><span class="string">should be empty</span><span class="punct">"</span> <span class="keyword">do</span>
<span class="attribute">@stack</span><span class="punct">.</span><span class="ident">should_be_empty</span>
<span class="keyword">end</span>
<span class="keyword">end</span> </code></pre></div>
<h4>Adding shared accessors</h4>
<p><code>spec_helper.rb</code> snippet:</p>
<div class="typocode"><pre><code class="typocode_ruby "><span class="keyword">module </span><span class="module">StackMethods</span>
<span class="ident">attr_accessor</span> <span class="symbol">:stack</span>
<span class="keyword">def </span><span class="method">push_an_object</span>
<span class="ident">stack</span> <span class="punct"><<</span> <span class="ident">mock</span><span class="punct">("</span><span class="string">some object</span><span class="punct">")</span>
<span class="keyword">end</span>
<span class="keyword">end</span>
<span class="keyword">class </span><span class="class">Spec::Runner::Context</span>
<span class="keyword">def </span><span class="method">before_context_eval</span>
<span class="attribute">@context_eval_module</span><span class="punct">.</span><span class="ident">include</span> <span class="constant">StackMethods</span>
<span class="keyword">end</span>
<span class="keyword">end</span></code></pre></div>
<p>Example spec:</p>
<div class="typocode"><pre><code class="typocode_ruby "><span class="ident">context</span> <span class="punct">"</span><span class="string">A stack with an object pushed</span><span class="punct">"</span> <span class="keyword">do</span>
<span class="ident">setup</span> <span class="keyword">do</span>
<span class="attribute">@stack</span> <span class="punct">=</span> <span class="constant">Stack</span><span class="punct">.</span><span class="ident">new</span>
<span class="keyword">end</span>
<span class="ident">specify</span> <span class="punct">"</span><span class="string">should not be empty</span><span class="punct">"</span> <span class="keyword">do</span>
<span class="ident">stack</span><span class="punct">.</span><span class="ident">should_be_empty</span>
<span class="ident">push_an_object</span>
<span class="ident">stack</span><span class="punct">.</span><span class="ident">should_not_be_empty</span>
<span class="keyword">end</span>
<span class="keyword">end</span> </code></pre></div>
<p>The examples are simple, but hopefully illustrate the techniques. For an example of some code that’s actually useful, check out my sample <a href="http://svn.caldersphere.net/svn/main/rspec_selenium_rc/trunk/">RSpec Selenium RC integration project</a>, in particular the <a href="http://svn.caldersphere.net/svn/main/rspec_selenium_rc/trunk/lib/spec_helper.rb">spec helper</a> and the <a href="http://svn.caldersphere.net/svn/main/rspec_selenium_rc/trunk/spec/example_spec.rb">example spec</a>. (More on this in the future if it proves useful, but for now if you check it out and run <code>rake</code> on it, it should launch <a href="http://www.openqa.org/selenium-rc/">Selenium RC</a> and run the example spec in a Firefox browser.)</p>
<p>By mixing and matching these techniques, you can layer a mini-DSL on top of RSpec and achieve DRY-er and even more readable and intention-revealing specs. Let me know if you’re able to find uses for these tips!</p>