Archive for August, 2011

Different ways of code reuse in RSpec

!!! THIS BLOG IS NO LONGER ACTIVE – CHECK OUT MY CURRENT BLOG AT VELESIN.IO !!!

Hierarchical structure of RSpec, with nested context/describe blocks and hierarchical before/after hooks, topped with a bit of syntactic sugar like let/let! methods, already goes a long way towards clean and readable tests. However, more complex suites still can quickly go un-DRY and unwieldy without code reuse between example groups.

Fortunately, RSpec provides several different tools for reusing test code: helper methods and modules, custom matchers, shared examples and shared contexts. It also allows to create examples programmatically (eg. in a loop). In this post I’ll briefly introduce all these options and discuss use cases for each of them.

RSpec helper methods and modules

To encapsulate common, complex logic RSpec allows you to write arbitrary helper methods directly within an example group (or in a parent example group):

describe "parent context" do
  def parent_helper_method
    # ...
  end

  describe "nested context" do
    def nested_helper_method
      # ...
    end

    it "should see both inherited and local helper methods" do
      parent_helper_method
      nested_helper_method
    end
  end
end

To make helper methods even more reusable (between multiple, not nested example groups), it is possible to define them in a module:

module Helpers
  def helper_method
    # ...
  end
end

and then include it in your example:

describe "some context" do
  include Helpers

  it "should see helper methods from included module" do
    helper_method
  end
end

You can also mix-in helpers via extend instead of include or include them through RSpec configuration instead of directly in an example group, but I won’t dig into all the details right now.

When should you use RSpec helper methods and modules?

Two areas where helper methods are most useful are example initialization and execution.

The primary use of helper methods is to hide implementation details by grouping several low-level statements into more meaningful, higher level abstractions (with clear, expressive names). There is really no magic here, this is analogical to the function of methods in OO programming. E.g. if you want to test different outcomes of the last round in a perfect darts game, instead of manually go through the whole play like this:

describe "last round in a perfect darts game"
  before do
    2.times do
      3.times do
        player_1.shoot(20, triple)
      end
      3.times do
        player_2.shoot(20, triple)
      end
    end
  end
end

you can do it this way:

describe "last round in a perfect darts game"
  before do
    2.times do
      shoot_a_perfect_round(player_1)
      shoot_a_perfect_round(player_2)
    end
  end
end

or even:

describe "last round in a perfect darts game"
  before do
    shoot_a_perfect_two_rounds_for_both_players
  end
end

using helper methods to hide all the nitty-gritty details of what constitutes a perfect round in darts.

In a similar manner, you can also use helper methods as factories, hiding construction of complex objects (although this use case is largely superseded by dedicated libraries like factory_girl or machinist).

RSpec custom matchers

RSpec also allows you to easily create new matchers to encapsulate custom assertion logic into well named methods:

RSpec::Matchers.define :be_thrice_as_big_as do |expected|
  match do |actual|
    actual == 3 * expected
  end
end

describe "number nine" do
  it "should be three times three" do
    9.should be_thrice_as_big_as 3
  end
end

As in the case of RSpec helpers, this is only a basic example. RSpec custom matchers are way more powerful than this, providing chaining, custom failure messages, their own internal helper methods and so on. They can be also defined as modules instead of RSpec::Matchers.define method.

When should you use RSpec custom matchers?

Use cases for custom matchers are quite similar to these of helper methods (hiding low-level details and providing meaningful naming). They differ in their area of use – while helper methods are used for example initialization and execution, custom matchers are used for example verification.

Custom matchers also provide an option to define custom failure messages, what (especially in the case of more complex, multi-step assertions) is a great way to improve readability and expressiveness not only of the test code but also of the test output.

RSpec shared examples

Another powerful approach to code reuse offered by RSpec is to extract complete sections of system under test specifications into shared examples, like this:

shared_examples_for User do
  it "behaves in some way" do
    # ...
  end

  it "behaves in some other way" do
    # ...
  end
end

You can then use such extracted specification to define behavior of other classes:

describe Customer do
  it_behaves_like User

  it "also behaves in special, Customer-y way" do
    # ...
  end
end

describe Admin do
  it_behaves_like User

  it "also behaves in special, Admin-y way" do
    # ...
  end
end

Such inclusion of shared examples works exactly like they were hand-coded inside a host example:

describe Customer do
  context "behaves like User" do
    it "behaves in some way" do
      # ...
    end

    it "behaves in some other way" do
      # ...
    end
  end

  it "also behaves in special, Customer-y way" do
    # ...
  end
end

Once more, this is only a tip of an iceberg – RSpec shared examples offer a lot of additional flexibility, e.g. you may pass them extension blocks or parameters, they may use methods defined in their host and they can be included without nesting – but the above examples illustrate the core idea.

When should you use RSpec shared examples?

Shared examples are especially useful for specifying inheritance hierarchies (see the example above with User, Customer and Admin), mixins and “interfaces”:

shared_examples_for Serializable do
  # ...
end

describe Dictionary do
  it_behaves_like Serializable
  # ...
end

describe TreeGraph do
  it_behaves_like Serializable
  # ...
end

Shared examples can also be used when specifying different states of a single object, that have some behavior in common, e.g.:

shared_examples_for Account do
  # ...
end

describe "new account" do
  it_behaves_like Account
  # ...
end

describe "confirmed account" do
  it_behaves_like Account
  # ...
end

describe "blocked account" do
  it_behaves_like Account
  # ...
end

RSpec shared contexts

Shared contexts are the newest addition to the RSpec code reuse mechanisms’ spectrum (added in RSpec 2.6). You can think of them as the opposite to shared examples – while shared examples allow you to inject examples that use their host’s context (before hooks, helper methods etc.), shared contexts allow you to inject context that will be used by their host’s examples.

Let’s look at how this is done. First, we define shared context:

shared_context "shared stuff" do
  before do
    @shared_var = :some_value
  end

  def shared_method
    # ...
  end

  let(:shared_let) { :some_value }
end

Then, we include it in our example group like this:

describe "group that includes a shared context" do
  include_context "shared stuff"

  it "has access to everything defined in shared context" do
    @shared_var
    shared_method
    shared_let
  end
end

Again, there is more to it, e.g. you can include shared context via metadata instead of explicit include_context, but I skip it for the sake of example brevity.

When should you use RSpec shared contexts?

Shared contexts are mostly useful when there are several examples that share some initial state, but they are too many, too complex or too unrelated to specify them in a single describe block through nesting.

This could be to some degree achieved also by using helper methods, but helper methods and shared contexts differ a bit in scope – shared contexts are higher level, containing several different initialization elements (before hooks, let and subject methods etc.) at once, while helpers are more narrowly focused and are used rather inside these initialization elements.

Constructing RSpec examples programmatically

One more way to help you DRY your examples is to create them programmatically. This is especially useful when done in a loop. Instead of creating a lot of almost identical examples by hand:

describe "pluralize" do
  it "should map mouse to mice" do
    "mouse".pluralize.should == "mice"
  end

  it "should map cactus to cacti" do
    "cactus".pluralize.should == "cacti"
  end

  it "should map tooth to teeth" do
    "tooth".pluralize.should == "teeth"
  end
end

You can do it in a much more concise way:

describe "pluralize" do
  {"mouse" => "mice", "cactus" => "cacti", "tooth" => "teeth"}.each do |singular, plural|
    it "should map #{singular} to #{plural}" do
      singular.pluralize.should == plural
    end
  end
end

When should you create RSpec examples programmatically?

Examples can be generated programmatically when they are data-driven, that is when there are several examples that express almost identical behaviour, but with different input/output values that all need to be tested to cover all important boundary conditions.

Coding all examples for such cases explicitly, by hand is too tedious and not DRY and testing them using only a single example with a loop inside, like this:

describe "pluralize" do
  it "should map singulars to plurals" do
    {"mouse" => "mice", "cactus" => "cacti", "tooth" => "teeth"}.each do |singular, plural|
      singular.pluralize.should == plural
    end
  end
end

although very concise, will result in harder to debug tests, because the example will be stopped at the first error encountered, instead of running through all the data every time.

Final words

My final words are simple: with all the various reuse tools RSpec puts in your tool-belt, there is no excuse to allow any kind of duplication to creep into your tests. Happy and DRY coding!

PS: There is one more code reuse tool provided by RSpec: macros. However, after recent enhancements to the shared examples mechanism (passing parameters, extension blocks etc.) they are no longer necessary (see RSpec author’s notes on this topic here) so I don’t cover them in this post.

Comments (15)