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:
- A class was created, based on the RSpec MatchArray, to do the comparison of the 2 arrays. It returns the result and error message.
- An assert method was created to execute the order-independent matching and interpret the results.
- A “infect_an_assertion” method was used to add our desired “must_match_array” to the Array class.
Should really use .to_set instead like so:
assert_equal expected.to_set, actual.set_set
another option could be to order (both) arrays and compare? if the same they should match right?