In the first part of this article we’ve met the MVO, which will help us to test separated states of an object. In this part, I will focus more on data_set as a list of elements that we are testing.
1. Permutation tables
The second good practice is a little more complex than the first one. We expect that we have a defined set of data in the input. Then we define the output of each set. And at the end, we assert data output. The best way to understand it is to see an example.
class User < ActiveRecord::Base
def full_name
[first_name, middle_name, last_name].compact.join(‘ ’)
end
end
The first thought that comes to mind is to add tests with different permutations of the variables. But the question is whether we should check all of the possible permutations. What if there are not 3 elements but for example 5?
describe ‘#full_name’ do
context ‘when first_name missing’ do
...
end
context ‘when middle_name missing’ do
...
end
context ‘when last_name missing’ do
...
end
context ‘when first_name and last_name missing’ do
...
end
...
end
Let’s try an approach, that is a little different. First, we build a data set for testing:
{[nil, ‘Adrian’, ‘Olesinski’] => ‘Adrian Olesinski’}.each do |name_set, output|
end
Now we have one option to test, but we can easily add all other options to be more readable.
{
[‘Przemek’, ‘Adrian’, ‘Olesinski’] => ‘Przemek Adrian Olesinski’,
[nil, ‘Adrian’, ‘Olesinski’] => ‘Adrian Olesinski’,
[‘Przemek’, nil, ‘Olesinski’] => ‘Przemek Olesinski’,
[‘Przemek’,’Adrian’, nil] => ‘Przemek Adrian’,
[nil, nil, ‘Olesinski’] => ‘Olesinski’,
[‘Przemek’, nil, nil] => ‘Przemek’,
[nil, ’Adrian’, nil’] => ‘Adrian’
}.each do |name_set, output|
end
When we have a data set and an output we can move the code to a shared example. This move will prevent us from testing the same method several times in a separate version.
describe ‘#full_name’ do
shared_examples_for ‘a full_name’ do |(first, middle, last), output|
subject(:full_name) { user.full_name }
let(:first_name) { first }
let(:middle_name) { middle }
let(:last_name) { last }
It { is_expected.to eq output }
end
{
[‘Przemek’, ‘Adrian’, ‘Olesinski’] => ‘Przemek Adrian Olesinski’,
[nil, ‘Adrian’, ‘Olesinski’] => ‘Adrian Olesinski’,
[‘Przemek’, nil, ‘Olesinski’] => ‘Przemek Olesinski’,
[‘Przemek’,’Adrian’, nil] => ‘Przemek Adrian’,
[nil, nil, ‘Olesinski’] => ‘Olesinski’,
[‘Przemek’, nil, nil] => ‘Przemek’,
[nil, ’Adrian’, nil’] => ‘Adriani’
}.each do |name_set, output|
it_behaves_like ‘a full_name’, name_set, output
end
end
If you would like to add more elements to the table in the future you just have to add them to the data set table. You don’t have to change the whole logic.
2. Golden Master
This time let’s start with a different approach. Let’s see in which cases this pattern will help us:
- A. Backfilling untested legacy code.
- B. Uncertain expectations requiring visual confirmation.
- C. Code complexity significantly exceeding current domain knowledge.
Now that we know where it can help us, we can see how the Golden Master (GM) works:
- Take a snapshot of an object (to a file).
- Verify the snapshot (manually).
- Compare future versions to the verified snapshot.
Now I could describe the whole idea, but because as a Ruby programmer I like to first check if there exists a solution already, I would like to recommend approvals gem. Don’t reinvent the wheel if you don’t have to.
When you decide to implement this pattern, you have to remember that you should not use it too often. Only use it for small isolated samples.
Summary
I hope that these three simple RSpec pattern examples will help you write better tests for everyday purposes. From my experience implementing the first pattern, MVO, helped to fix over 74% of tests that I inherited after other programmers. So it’s definitely worth a try.
Author: Przemek Olesiński