Saying stuff about stuff.

Testing an array of objects with RSpec have_attributes

After recently discovering RSpec’s --next-failure option I’ve just happened upon the have_attributes matcher which can help turn many expectations into a single, more readable statement.

In the past when checking an array of objects I’ve manually written out each expectation, something like this:

expect(items[0].id).to eql(1)
expect(items[0].name).to eql('One')
expect(items[1].id).to eql(2)
expect(items[1].name).to eql('Two')

But have_attributes lets you check an object’s methods against a hash, so the above can be re-written as:

expect(items[0]).to have_attributes(id: 1, name: 'One')
expect(items[1]).to have_attributes(id: 2, name: 'Two')

Even better, have_attributes can be combined with match_array to get this:

expect(items).to match_array([
  have_attributes(id: 1, name: 'One'),
  have_attributes(id: 2, name: 'Two'),
])

In one particular case I also wanted to check that the correct class was being returned, which is simple as it’s just another method call:

expect(items).to match_array([
  have_attributes(class: Foo, id: 1, name: 'One'),
  have_attributes(class: Bar, id: 2, name: 'Two'),
])

The next thing I tried felt quite natural though I didn’t expect it to work:

expect(items).to match_array([
  have_attributes(
    class: Foo,
    id: 1,
    name: 'One',
    price: have_attributes(
      cents: 123,
      currency: 'GBP',
    ),
  ),
  have_attributes(
    class: Bar,
    id: 2,
    name: 'Two',
    price: have_attributes(
      cents: 456,
      currency: 'USD',
    ),
  ),
])

It turns out this almost exactly matches the examples from the docs — I guess I wasn’t paying much attention all those years ago when RSpec announced composable matchers.