Rahul
Rahul

Reputation: 321

use rspec built in matcher within custom matcher

I have a test suite with too many assertions like this

expect(array_1).to match_array array_2
expect(array_3).to match_array array_4
expect(array_5).to match_array array_5

and so forth.

I'd like to wrap these checks in a custom matcher but within that custom matcher would like to use the match_array matcher as I really like the error message it returns listing missing and extra elements in case of a mismatch.

Something like this:

RSpec::Matchers.define :have_data do |data|
  match do |actual|
    actual.eql? data # I don't want to do this.
    actual.match_array? data # <<- I'd like do do something like this to retain the matcher behaviour
  end
end

Any way I can do this? match_array? doesn't exist, of course.

Upvotes: 1

Views: 900

Answers (2)

E L Rayle
E L Rayle

Reputation: 1201

I wanted to use the contain_exactly functionality with a remapping of the actual array to ids for the array items. This is similar to what you are trying to do, so perhaps it will help. I put this in /support/matchers/match_array_ids.rb.

module MyMatchers
  class MatchArrayIds < RSpec::Matchers::BuiltIn::ContainExactly
    def match_when_sorted?
      values_match?(safe_sort(expected), safe_sort(actual.map(&:id)))
    end
  end

  def match_array_ids(items)
    MatchArrayIds.new(items)
  end
end

I also created a test to test the matcher was functioning as expected. I put this in spec/matcher_tests/match_array_ids_spec.rb.

# frozen_string_literal: true
require 'spec_helper'

RSpec.describe MyMatchers::MatchArrayIds do
  before do
    class ObjectWithId
      attr_reader :id
      def initialize(id)
        @id = id
      end
    end
  end

  context 'when array of objects with ids and array of ids match' do
    let(:objects_with_ids) { [ObjectWithId.new('id1'), ObjectWithId.new('id2')] }
    let(:ids) { ['id1', 'id2'] }

    it 'returns true' do
      expect(objects_with_ids).to match_array_ids(ids)
    end
  end

  context 'when array of objects with ids exist and array of ids is empty' do
    let(:objects_with_ids) { [ObjectWithId.new('id1'), ObjectWithId.new('id2')] }
    let(:ids) { [] }

    it 'returns false' do
      expect(objects_with_ids).not_to match_array_ids(ids)
    end
  end

  context 'when array of objects with ids is empty and array of ids exist' do
    let(:objects_with_ids) { [] }
    let(:ids) { ['id1', 'id2'] }

    it 'returns false' do
      expect(objects_with_ids).not_to match_array_ids(ids)
    end
  end

  context 'when array of objects with ids and array of ids DO NOT match' do
    let(:objects_with_ids) { [ObjectWithId.new('id1'), ObjectWithId.new('id2')] }
    let(:ids) { ['id1', 'id3'] }

    it 'returns false' do
      expect(objects_with_ids).not_to match_array_ids(ids)
    end
  end
end

You can find the examples for making a custom from scratch matcher that I based this on at... https://github.com/rspec/rspec-expectations/blob/45e5070c797fb4cb6166c8daca2ea68e31aeca40/lib/rspec/matchers.rb#L145-L196

Upvotes: 1

Rodrigo Vasconcelos
Rodrigo Vasconcelos

Reputation: 1290

Looking at the implementation of the match_array matcher:

# An alternate form of `contain_exactly` that accepts
# the expected contents as a single array arg rather
# that splatted out as individual items.
#
# @example
#   expect(results).to contain_exactly(1, 2)
#   # is identical to:
#   expect(results).to match_array([1, 2])
#
# @see #contain_exactly
def match_array(items)
  contain_exactly(*items)
end

And the contain_exactly method makes a call to the Rspec::BuiltIn::ContainExactly module:

def contain_exactly(*items)
  BuiltIn::ContainExactly.new(items)
end 

You could iterate over the data you need to match using calls to this module, and still use the error messages from the match_array method. Alternatively, you could implement your own module based on the BuiltIn::ContainExactly module.

Upvotes: 1

Related Questions