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:
- Object composition, the motivation for shared examples, in Ruby
- Testing object composition without shared examples
- 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:
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:
- Don't have to copy-and-paste the same code in two locations
- 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.