Как утверждать содержимое массива, независимо от порядка

У меня есть минимальная спецификация:

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

Это, однако, терпит неудачу:

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

Я мог бы заказать их обоих, но это добавляет беспорядок:

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

Как бы то ни было, map{} уже не имеет никакого отношения к тесту и добавляет беспорядок, я бы предпочел не добавлять еще больше.

Есть ли утверждение или помощник для проверки, находятся ли все элементы в enumerator1 в enumerator2?

Ответы

Ответ 1

TL; DR. Самый прямой способ проверить это - отсортировать массивы перед проверкой их равенства.

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

Еще здесь? Хорошо. Давайте поговорим о сравнении элементов в массиве.

Как бы то ни было, отображение {} уже несколько не имеет отношения к тесту и добавляет беспорядок, я бы предпочел не добавлять еще больше.

Ну, это часть проблемы. Ваш JSON содержит массив объектов JSON, а вызов Database.pluck будет возвращать что-то еще, предположительно целые. Вам нужно преобразовать объекты JSON и ваш запрос в один и тот же тип данных. Поэтому неточно сказать, что .map{} не имеет значения, и если это кажется беспорядочным, то это потому, что вы делаете так много вещей в своем утверждении. Попробуйте разделить эту строку кода и использовать раскрывающиеся имена целей:

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

Это больше строк кода в вашем тесте, но лучше передает намерение. И все же я слышу ваши слова "нерелевантные" и "беспорядок", повторяющиеся в моем сознании. Держу пари, вам не нравится это решение. "Слишком много работы!" И "Почему я должен отвечать за это?" Ладно ладно. У нас больше возможностей. Как насчет более умного утверждения?

RSpec имеет приятный маленький помощник с именем match_array, который делает в значительной степени то, что вы ищете. Он сортирует и сравнивает массивы и выводит хорошее сообщение, если оно не соответствует. Мы могли бы сделать что-то подобное.

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

"Но это утверждение, а не ожидание!" Да, знаю. Расслабьтесь. Вы можете включить утверждение в ожидании, вызвав infect_an_assertion. Но для этого вы, вероятно, захотите добавить метод assertion, чтобы он мог использоваться в каждом тесте Minitest. Поэтому в моем test_helper.rb файле я бы добавил следующее:

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

Теперь ваше утверждение можно использовать в любом тесте, и ваше ожидание будет доступно для каждого объекта.

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

Ответ 2

MiniTest Rails assert_same_elements утверждение assert_same_elements, которое:

Утверждает, что два массива содержат одинаковые элементы, одинаковое количество раз. По сути ==, но неупорядоченный.

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

Ответ 3

RSpec имеет совпадение match_array, которое выполняет сопоставление двух массивов независимо от порядка. Для создания аналогичного пользовательского совпадения в 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

Вы можете увидеть этот пользовательский матчи в действии в следующем тесте:

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

Итак, с помощью этого пользовательского совпадения вы можете повторно написать свой тест как:

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

Ответ 4

Вы можете использовать вычитание массива в Ruby следующим образом:

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

Но, пожалуйста, учтите следующее: ["A", "B"] - ["B", "A"] == [] НО ["A", "B", "B"] - ["B", "A"] == []

Так что используйте эту технику, только когда у вас есть уникальные значения.

Ответ 5

В тестовом сценарии, где производительность не критична, вы можете использовать итерации и assert_include, например:

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

где test_result_items массив с результатами кода тестируемого, и expected_items массив с ожидаемыми элементов (в любом порядке).

Чтобы убедиться, что присутствуют все элементы (и нет дополнительных элементов), объедините вышеприведенное с проверкой длины массива:

assert_equal expected_items.length, test_result_items.length

Обратите внимание, что это только установит, что эти два массива равны, если элементы уникальны. (Поскольку у test_result_items с ['a', 'a', 'a'] действительно есть только элементы, которые присутствуют в expected_items элементах ['a', 'b', 'c'].)

Ответ 6

Одним из вариантов является использование наборов, если повторение не является проблемой (стандартный ruby)

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

Другой мудрый напишите свой собственный метод assert (из musta)

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!

Или добавьте Shoulda камень прямо на многое другое.