Writing Custom RSpec Matchers

RSpec matchers let you abstract away common assertions in your test code.

For example, we recently had a spec file with a bunch of lines that looked like this:

1
worksheet.rows[0].cells.map(&:value).should include "Foo"

Which tests if the excel file we’re generating (using axlsx) includes Foo in the header row.

That isn’t very neat. What if we replace it with this?

1
worksheet.should have_header_cell "Foo"

That looks a lot better. We can implement this kind of abstraction using custom RSpec matchers.

The matcher for this is as simple as:

1
2
3
4
5
RSpec::Matchers.define :have_header_cell do |cell_value|
  match do |worksheet|
    worksheet.rows[0].cells.map(&:value).include? cell_value
  end
end

RSpec passes in the expected and actual values to these blocks, and our code has to return a boolean representing the result of the assertion.

Now what about assertions that look like this?

1
2
3
worksheet.rows[1].cells.map(&:value).should include "Foo"
worksheet.rows[2].cells.map(&:value).should include "Bar"
worksheet.rows[3].cells.map(&:value).should include "Baz"

The row that we’re checking changes for each assertion. Of course, we could create a different matcher for each of these cases, but there’s a better way.

1
2
3
worksheet.should have_cell("Foo").in_row 1
worksheet.should have_cell("Bar").in_row 2
worksheet.should have_cell("Baz").in_row 3

RSpec lets you chain custom matchers.

1
2
3
4
5
6
7
8
9
10
11
12
13
RSpec::Matchers.define :have_cell do |expected|
  match do |worksheet|
    worksheet.rows[@index].cells.map(&:value).include? expected
  end

  chain :in_row do |index|
    @index = index
  end

  failure_message_for_should do |actual|
    "Expected #{actual} to include #{expected} at row #{@index}."
  end
end

We first store the argument passed in to in_row as an instance variable, and then access it in the main have_cell matcher.

The example also includes a custom error message handler, which properly formats an error message if the assertion fails.

Comments