RSpec should each matcher

Motivation

One common testing situation is running an assertion over each element of a collection.

For a truly contrived example, lets test an array to see that every element is the number 1.
Traditional RSpec:

describe 'array of ones' do
  it 'should be an array of ones' do
    [1,2,3].each do |n| 
      n.should == 1
    end
  end

  # or #

  [1,2,3].each do |n| 
    it 'should be an array of ones' do
      n.should == 1
    end
  end
end

Problem

The problem with these tests is knowing which element failed. In real usage, the array won’t be a known literal, and our assertions may be more complex.
The second method is tempting because the spec runner will count more tests. However, testing is supposed to be simple and consistent, and generating tests on the fly is not. There will also be a problem of tests with the same name.

Solution

Unfortunately, RSpec doesn’t give quite the right tools for this job. But we can write custom matchers, like the following should each matcher.

describe 'array of ones' do
  it 'should be an array of ones' do
    [1,2,3].should each { |n| 
      n.should == 1
    }
  end
end

should each allows you to write the test in the same natural way, but gives useful and accurate information on failure.

'array of ones should fail on 2' FAILED
    line: 14
  item 1: 2
expected: 1,
     got: 2 (using ==)

As expected, the output shows expected and got fields. line is the line number of the expectiation inside the block. This allows you to write as many should assertions and the block and know exactly which line it failed on. The item line gives the index of the item being yielded to the block (in this case 1), and the item itself (in this case 2)

Warning!

note the use of brackets { ... } instead of do ... end. This is necessary because do .. end does not bind strongly enough. If you mess this up you will get a nice warning message, so no big deal.

Implementation

RSpec custom matchers are easy to write. The small amount of time to write a custom matcher pays off by making specs more concise and clear.

However, RSpec matchers were not designed for this type of capability, so I had to couple the implementation of this custom matcher to RSpec. When an assertion fails, an exception of class Spec::Expectations::ExpectationNotMetError is raised. So I just trap that exception. To get the line number of the assertion failure I look for matches? in statck trace of that exception and move back in the stack trace to the last line number.

Get it

rspec-multi-matchers on gitgub

© Greg Weber. All original code snippets are placed in the public domain.
Written on Wed Nov 5 20:31:20 2008.
Tagged as rspec, ruby, test.
Send author an email.