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.