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.
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.