Hands-On: Shared Examples in RSpec

This post is the first in a series on shared examples in RSpec and Kiwi. It takes approximately 4 minutes to read, and it examines:

  1. Object composition, the motivation for shared examples, in Ruby
  2. Testing object composition without shared examples
  3. Testing object composition with shared examples

You should know basic Ruby syntax to get the most from this post.

The Gist

Shared examples make testing compositions of objects much easier. In a nutshell, they allow us to execute the same group of expectations against several classes, like so:

shared_examples 'an acceleratable' do
  let(:acceleratable) { described_class.new }

  describe 'speed' do
    it 'is initially zero' do
      acceleratable.speed.should == 0
    end
  end
end

describe Car do
  it_behaves_like 'an acceleratable'
end

describe Motorcycle do
  it_behaves_like 'an acceleratable'
  describe 'other methods' do
    # ...
  end
end

An example of object composition in Ruby using mixins

Chances are you've read – or learned the hard way – that one should favor composition over inheritance. The basic premise behind the idea is that it's easier to maintain a large class composed of many small modules than it is to maintain a class derived from a long list of parent classes.

Ruby makes composition easy: just use mixins. The following Ruby code, for example, defines a mixin called Acceleratable. Instances of classes that incorporate this mixin can speed up by using the #accelerate! method.

# acceleratable.rb

module Acceleratable
  attr_reader :speed

  def initialize
    @speed = 0
  end

  def accelerate!
    @speed += 10
  end
end

Using the Acceleratable mixin, it's easy to create two classes which can accelerate: a Car and a Motorcycle:

# car.rb

require 'acceleratable'

class Car
  include Acceleratable
end
# motorcycle.rb

require 'acceleratable'

class Motorcycle
  include Acceleratable

  def wheelie!
    puts 'Check out this sick wheelie, bro!'
    @speed -= 5
  end
end

However, we encounter a problem when we wish to test the behavior of these objects: we can't test the mixin itself. In order to test the two classes which use the mixin, we have to write each test twice! But why is this the case?

A naive approach to testing composite objects

Acceleratable is a mixin. That is, it's meant to be incorporated into other classes, not used on its own. It doesn't make sense to test an Acceleratable–whatever that means. We can test a Car or a Motorcycle, though.

Let's begin with Car. The spec below makes sure that a new car starts out parked with a speed of 0. Calling #accelerate! takes it out of park and gets it rolling down the road with a speed of 10:

# car_spec.rb

require 'car'

describe Car do
  let(:car) { described_class.new }

  describe 'speed' do
    it 'is initially zero' do
      car.speed.should == 0
    end

    it 'increases upon acceleration' do
      car.accelerate!
      car.speed.should == 10

      car.accelerate!
      car.speed.should == 20
    end
  end
end

Next, let's test Motorcycle. In addition to testing the #wheelie! method, we also want to make sure it responds to #accelerate! just like Car does. As a result, our tests contain a lot of repetition:

# motorcycle_spec.rb

describe Motorcycle do
  let(:motorcycle) { described_class.new }

  describe 'wheelie!' do
    it 'decreases the speed' do
      motorcycle.accelerate!
      motorcycle.wheelie!
      motorcycle.speed.should == 5
    end
  end

  # !!!
  # These tests are identical to the ones
  # in `car_spec.rb`!
  describe 'speed' do
    it 'is initially zero' do
      car.speed.should == 0
    end

    it 'increases upon acceleration' do
      car.accelerate!
      car.speed.should == 10

      car.accelerate!
      car.speed.should == 20
    end
  end  
  # !!!
end

Writing two sets of the same tests is tedious and difficult to maintain. Furthermore, we'll need to copy-and-paste the same set of tests for all classes that use the Acceleratable mixin. There's got to be a better way, right?

Shared examples are the answer.

Using shared examples to test composite objects

The tests for the #accelerate! method are practically identical in Car and Motorcycle. Using the #shared_examples method, we can extract these tests into a single location:

# acceleratable_spec_helper.rb

shared_examples 'an acceleratable' do
  let(:acceleratable) { described_class.new }

  describe 'speed' do
    it 'is initially zero' do
      acceleratable.speed.should == 0
    end
  end
end

We can then execute the expectations against Car and Motorcycle by using the #it_behaves_like method:

# car_spec.rb

require 'car'
require 'acceleratable_spec_helper'

describe Car do
  it_behaves_like 'an acceleratable'
end
# motorcycle_spec.rb

require 'motorcycle'
require 'acceleratable_spec_helper'

describe Motorcycle do
  it_behaves_like 'an acceleratable'
  describe 'other methods' do
    # ...
  end
end

By extracting the shared tests into a single location, we:

  1. Don't have to copy-and-paste the same code in two locations
  2. Don't have to change two files if we want to change the #accelerate! tests somehow

Hopefully you can see how shared examples might be useful. In the next post in this series, we examine how RSpec implements shared examples rspec-core.

More on RSpec