How to write better rspec tests - part 2

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.

alt

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

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:

  1. Take a snapshot of an object (to a file).
  2. Verify the snapshot (manually).
  3. 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.

Contact Us

Oops, there was an error sending your message. Try again.
Sending...
Thanks for getting in touch! We'll get back to you as soon as possible.