Question

I have a minitest spec:

it "fetches a list of all databases" do
  get "/v1/databases"
  json = JSON.parse(response.body)
  json.length.must_equal           Database.count
  json.map{|d| d["id"]}.must_equal Database.all.pluck(:id)
end

This, however, fails:

Expected: [610897332, 251689721]
  Actual: [251689721, 610897332]

I could order them both, but that adds clutter:

json.map{|d| d["id"]}.sort.must_equal Database.all.pluck(:id).sort

As it is, the map{} is already somewhat irrelevant to the test and adding clutter, I'd prefer to not add even more.

Is there an assertion or helper to test if all items in enumerator1 are all in enumerator2?

Was it helpful?

Solution

TL;DR The most direct way to check this is to sort the arrays before checking their equality.

json.map{|d| d["id"]}.sort.must_equal Database.all.pluck(:id).sort

Still here? Okay. Let's talk about comparing elements in an array.

As it is, the map{} is already somewhat irrelevant to the test and adding clutter, I'd prefer to not add even more.

Well, that is part of the problem. Your JSON contains an array of JSON objects, while calling Database.pluck will return something else, presumably integers. You need to convert your JSON objects and your query to be the same datatype. So it's not accurate to say that the .map{} is irrelevant, and if it feels like clutter then that is because you are doing so many things in your assertion. Try splitting that line of code apart and using intention revealing names:

sorted_json_ids = json.map{|d| d["id"]}.sort
sorted_db_ids   = Database.order(:id).pluck(:id)
sorted_json_ids.must_equal sorted_db_ids

It is more lines of code in your test, but better communicates the intent. And yet I hear your words "irrelevant" and "clutter" echoing in my mind. I bet you don't like this solution. "Its too much work!" And "Why do I have to be responsible for this?" Okay, okay. We have more options. How about a smarter assertion?

RSpec has a nice little matcher named match_array that does pretty much what you are looking for. It sorts and compares arrays and prints a nice message if they don't match. We could do something similar.

def assert_matched_arrays expected, actual
  assert_equal expected.to_ary.sort, actual.to_ary.sort
end

it "fetches a list of all databases" do
  get "/v1/databases"
  json = JSON.parse(response.body)
  assert_matched_arrays Database.pluck(:id), json.map{|d| d["id"]}
end

"But that's an assertion and not an expectation!" Yeah, I know. Relax. You can turn an assertion into an expectation by calling infect_an_assertion. But to do this right, you probably want to add the assertion method so it can be used in every Minitest test. So in my test_helper.rb file I'd add the following:

module MiniTest::Assertions
  ##
  # Fails unless <tt>exp</tt> and <tt>act</tt> are both arrays and
  # contain the same elements.
  #
  #     assert_matched_arrays [3,2,1], [1,2,3]

  def assert_matched_arrays exp, act
    exp_ary = exp.to_ary
    assert_kind_of Array, exp_ary
    act_ary = act.to_ary
    assert_kind_of Array, act_ary
    assert_equal exp_ary.sort, act_ary.sort
  end
end

module MiniTest::Expectations
  ##
  # See MiniTest::Assertions#assert_matched_arrays
  #
  #     [1,2,3].must_match_array [3,2,1]
  #
  # :method: must_match_array

  infect_an_assertion :assert_matched_arrays, :must_match_array
end

Now your assertion can be used in any test, and your expectation will be available on every object.

it "fetches a list of all databases" do
  get "/v1/databases"
  json = JSON.parse(response.body)
  json.map{|d| d["id"]}.must_match_array Database.pluck(:id)
end

OTHER TIPS

MiniTest Rails Shoulda has an assert_same_elements assertion, which:

Asserts that two arrays contain the same elements, the same number of times. Essentially ==, but unordered.

assert_same_elements([:a, :b, :c], [:c, :a, :b]) => passes

One option is to use sets if repetition is not an issue (standard ruby)

 require 'set'
 assert_equals [1,2,3].to_set, [3,2,1].to_set

Other wise write your own assert method (from shoulda)

module Minitest::Assertions
  def assert_same_elements(expected, current, msg = nil)
    assert expected_h = expected.each_with_object({}) { |e, h| h[e] ||= expected.select { |i| i == e }.size }
    assert current_h = current.each_with_object({}) { |e, h| h[e] ||= current.select { |i| i == e }.size}

    assert_equal(expected_h, current_h, msg)
  end
end

assert_same_elements [1,2,3,3], [3,2,1,3] # ok!
assert_same_elements [1,2,3,3], [3,2,1] # fails!

Or add shoulda gem directly for much more.

RSpec has a match_array matcher that does matching of 2 arrays regardless of order. You could do the following to create a similar custom matcher in Minitest:

module MiniTest::Assertions

  class MatchEnumerator
    def initialize(expected, actual)
      @expected = expected
      @actual = actual
    end

    def match()
      return result, message
    end

    def result()
      return false unless @actual.respond_to? :to_a
      @extra_items = difference_between_enumerators(@actual, @expected)
      @missing_items = difference_between_enumerators(@expected, @actual)
      @extra_items.empty? & @missing_items.empty?      
    end

    def message()
      if @actual.respond_to? :to_a
        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_enumerators(array_1, array_2)
      difference = array_1.to_a.dup
      array_2.to_a.each do |element|
        if index = difference.index(element)
          difference.delete_at(index)
        end
      end
      difference
    end
  end # MatchEnumerator

  def assert_match_enumerator(expected, actual)
    result, message = MatchEnumerator.new(expected, actual).match
    assert result, message
  end

end # MiniTest::Assertions

Enumerator.infect_an_assertion :assert_match_enumerator, :assert_match_enumerator

You can see this custom matcher in action in the following test:

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

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

So, with this custom matcher, you could re-write your test as:

it "fetches a list of all databases" do
  get "/v1/databases"
  json = JSON.parse(response.body)
  json.length.must_equal           Database.count
  json.map{|d| d["id"]}.must_match_enumerator Database.all.pluck(:id)
end

You can make use of array substraction in Ruby like this:

assert_empty(["A", "B"] - ["B", "A"])

But please be aware of the following: ["A", "B"] - ["B", "A"] == [] BUT ["A", "B", "B"] - ["B", "A"] == []

So only use this technique when you have unique values in there.

In a test scenario where performance isn't critical, you could use iteration and assert_include, for example:

test_result_items.each { |item| assert_include(expected_items, item) }

where test_result_items is an array with the results of the code under test, and expected_items is an array with your expected items (in any order).

To ensure that all items are present (and no extra items are present), combine the above with a check on the array lengths:

assert_equal expected_items.length, test_result_items.length

Note that this will only establish that the two arrays are equal if the items are unique. (Because a test_result_items with ['a', 'a', 'a'] does indeed have only items that are present in an expected_items of ['a', 'b', 'c'].)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top