Comparing arrays in an order-independent manner using minitest

Problem

After reading this question on StackOverflow, I was curious how you could compare 2 arrays in an order-independent way. For example, the spec:

describe "order-independent comparison" do
  it{ [1, 2, 3].must_equal [2, 3, 1] }
end

Will fail with the exception:

  1) Failure:
order-independent comparison#test_0001_anonymous [stuff_enumerator.rb:80]:
Expected: [2, 3, 1]
  Actual: [1, 2, 3]

We want the spec to pass since all elements in the first array are in the second array – ie the order should not matter. There does not a appear to be a built-in matcher for this.

Solution 1 – Iterate Through Each Element

Minitest does have a “must_include” matcher that checks that the array includes a particular element. This gives us a possible solution of iterating through each expected element to verify that it is included:

describe "order-independent comparison" do
  expected_array = [2, 3, 1]
  expected_array.each do |value|
    it{ [1, 2, 3].must_include value }  
  end
end

While this now passes, there are a couple of disadvantages. The first being that the actual array might include elements that are not in the expected array. The other disadvantage is that it is a lot of writing if used in multiple tests.

Solution 2 – Custom Matcher

A nicer approach would be to create a custom matcher similar to that used in RSpec.

First off, let us start with a spec that shows what the matcher should do:

describe "must_match_array" do
  it{ [1, 2, 3].must_match_array [1, 2, 3] }
  it{ [1, 2, 3].must_match_array [1, 3, 2] }
  it{ [1, 2, 3].must_match_array [2, 1, 3] }
  it{ [1, 2, 3].must_match_array [2, 3, 1] }
  it{ [1, 2, 3].must_match_array [3, 1, 2] }
  it{ [1, 2, 3].must_match_array [3, 2, 1] }

  # deliberate failures
  it{ [1, 2, 3].must_match_array [1, 2, 1] }
end

Note that “must_match_array” is the custom matcher that we want to exist.

The custom matcher can be created as:

module MiniTest::Assertions

  class MatchArray
    def initialize(expected, actual)
      @expected = expected
      @actual = actual
    end
    
    def match()
      return result, message
    end
    
    def result()
      return false unless @actual.respond_to? :to_ary
      @extra_items = difference_between_arrays(@actual, @expected)
      @missing_items = difference_between_arrays(@expected, @actual)
      @extra_items.empty? & @missing_items.empty?      
    end

    def message()
      if @actual.respond_to? :to_ary
        message = "expected collection contained: #{safe_sort(@expected).inspect}\n"
        message += "actual collection contained: #{safe_sort(@actual).inspect}\n"
        message += "the missing elements were: #{safe_sort(@missing_items).inspect}\n" unless @missing_items.empty?
        message += "the extra elements were: #{safe_sort(@extra_items).inspect}\n" unless @extra_items.empty?
      else
        message = "expected an array, actual collection was #{@actual.inspect}"
      end

      message
    end
    
    private

    def safe_sort(array)
      array.sort rescue array
    end

    def difference_between_arrays(array_1, array_2)
      difference = array_1.to_ary.dup
      array_2.to_ary.each do |element|
        if index = difference.index(element)
          difference.delete_at(index)
        end
      end
      difference
    end
  end # MatchArray

  def assert_match_array(expected, actual)
    result, message = MatchArray.new(expected, actual).match
    assert result, message
  end
  
end # MiniTest::Assertions

Array.infect_an_assertion :assert_match_array, :must_match_array

Notice that this code has three parts:

  1. A class was created, based on the RSpec MatchArray, to do the comparison of the 2 arrays. It returns the result and error message.
  2. An assert method was created to execute the order-independent matching and interpret the results.
  3. A “infect_an_assertion” method was used to add our desired “must_match_array” to the Array class.
This entry was posted in Minitest. Bookmark the permalink.

2 Responses to Comparing arrays in an order-independent manner using minitest

  1. Kyle says:

    Should really use .to_set instead like so:
    assert_equal expected.to_set, actual.set_set

  2. Ben says:

    another option could be to order (both) arrays and compare? if the same they should match right?

Leave a comment