Как утверждать содержимое массива, независимо от порядка
У меня есть минимальная спецификация:
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 камень прямо на многое другое.