Hire Us
Using Configurable Shared Examples in RSpec

Using Configurable Shared Examples in RSpec

Shared examples are a good tool to describe some complex behavior and reuse it across different parts of a spec. Things get more complicated when you have the same behavior, but it has some slight variations for different contexts. In this case it’s easy to end-up having a bunch of separate one-spec behaviors instead of having some way to adjust those peculiarities in a simple way.

Example

To be more specific, let’s create a simple example. We want to describe behavior of different dogs. Every one of them can perform some common actions, but there’re also certain actions they don’t perform at all. For example, Snuff can bark and growl, but doesn’t like to jump. On the other hand, Scooby-Doo likes to jump and flee, but doesn’t growl. Scrappy-Doo is too small to bark, but he likes to growl a lot.

class Dog < Struct.new(:able_to_growl?, :able_to_bark?, :able_to_jump?, :able_to_flee?)
end
snuff = Dog.new(true, true, false, true)
scooby_doo = Dog.new(false, true, true, true)
scrappy_doo = Dog.new(true, false, true, true)

shared_examples 'a normal dog' do
  it { is_expected.to be_able_to_growl }
  it { is_expected.to be_able_to_bark }
  it { is_expected.to be_able_to_jump }
  it { is_expected.to be_able_to_flee }
end

describe 'Dogs behavior' do
  context 'Snuff' do
    subject(:snuff) { Dog.new(true, true, false, true) }
    it_behaves_like 'a normal dog'
  end

  context 'Scooby-Doo' do
    subject(:scooby_doo) { Dog.new(false, true, true, true) }
    it_behaves_like 'a normal dog'
  end

  context 'Scrappy-Doo' do
    subject(:scrappy_doo) { Dog.new(true, false, true, true) }
    it_behaves_like 'a normal dog'
  end
end

The spec looks great, but we should adjust ‘a normal dog’ behavior for each character, otherwise each context will fail due to unsupported ability.

Using params in shared_examples

Luckily, RSpec supports accepting params for shared_examples, so we can rewrite the spec like this:

shared_examples 'a normal dog' do |growl: true, bark: true, jump: true|
  it { is_expected.to be_able_to_growl } if growl
  it { is_expected.to be_able_to_bark } if bark
  it { is_expected.to be_able_to_jump } if jump
  it { is_expected.to be_able_to_flee }
end

describe 'Dogs behavior' do
  context 'Snuff' do
    subject(:snuff) { Dog.new(true, true, false, true) }
    it_behaves_like 'a normal dog', jump: false
  end

  context 'Scooby-Doo' do
    subject(:scooby_doo) { Dog.new(false, true, true, true) }
    it_behaves_like 'a normal dog', growl: false
  end

  context 'Scrappy-Doo' do
    subject(:scrappy_doo) { Dog.new(true, false, true, true) }
    it_behaves_like 'a normal dog', bark: false
  end
end

Now all specs pass successfully. We use shared_examples params to pass configuration values and adjust the way it matches. Note that this won’t work with usual let-bindings. Shared examples are created and configured at the “compile time”, while let-bindings can be used only at “run time”.

Another nice thing about shared_examples params – is that they are accessible within tests at run-time. So, for very simple specs it’s possible to use them instead of let-bound values. Here is a simple example:

shared_examples 'multiplies two numbers' do |x, y, result:|
  it 'returns correct result' do
    expect(x * y).to eq(result)
  end
end

describe 'Multiplication' do
  it_behaves_like 'multiplies two numbers', 2, 2, result: 4
  it_behaves_like 'multiplies two numbers', 3, 5, result: 15
  it_behaves_like 'multiplies two numbers', 10, 5, result: 50
end

So, instead of using 3 let-bindings for the arguments and the result, we get a very succinct way of writing simple specs.