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.