assert_maps_equal should have a default third argument
I think this https://github.com/devonestes/assertions/blob/8b353fd941baee8b71181d4e8722c587a5aede87/lib/assertions.ex#L333 should have a sensible default:
if nothing is passed as keys, assume exact equivalency
-
assert_lists_equal Map.keys(left), Map.keys(right)at the beginning - then the deeper check key by key
if :left is passed as third argument, should convert to Maps.keys(left) and equivalently for right
Or something along those lines. What do you think? Would a PR in this sense be useful/appreciated?
Not sure if this is related, but I came here because something that I struggle with a lot is wanting to be able to check if a list of results match a set of subsets because one or more keys will change from test-to-test (like an ID or a timestamp). For example, if I have a function foo which returns a list of maps:
foo(...)
> [%{id: 12, name: "John", cool?: true}, %{id: 15, name: "Sally", cool?: true}]
I could do a couple of different things:
assert length(result) == 2
Enum.any?(result, & match?(%{name: "John", cool?: false}, &1))
Enum.any?(result, & match?(%{name: "Sally", cool?: true}, &1))
Or maybe:
assert_lists_equal Enum.map(results, & &1.name) == ["John", "Sally"]
assert_lists_equal Enum.map(results, & &1.cool) == [false, true]
But it would be nice to maybe have something like:
assert_lists_match results, [%{name: "John", cool?: true}, %{name: "Sally", cool?: true}]
One question is if each element of the list should match according to match? (maybe macro magic would be needed?) or if it would be enough to assume that each element in the given list is a map or struct and that the maps given in the assertion are basically a compact way of asserting that those keys have those values. So it might be a bit like the assert_lists_equal example above, except that rather than just making sure there is the right set of values for each key you'd be checking that John is not cool and Sally is cool.
It might look something like this:
def assert_lists_match([], []), do: true
def assert_lists_match(records, asserted_maps) when length(records) != length(asserted_maps), do: false
def assert_lists_match(records, [asserted_map | rest_of_assertion]) do
index = Enum.find_index(records, fn record ->
Enum.all?(asserted_map, fn {asserted_key, asserted_value} ->
record[asserted_key] == asserted_value
end)
end)
if index do
assert_lists_match(List.delete_at(records, index), rest_of_assertion)
else
false
end
end
I made a local function for myself to get on with what I was doing and I have some tests behind it now, so my draft code changed some. I needed to take care of the problem where matching records get exhausted early, so it needs to be open to looking through the whole search space.
Also, I put the assertion-part as the first argument since that seemed to be how the assertions library leans.
def assert_lists_match([], []), do: true
def assert_lists_match(asserted_maps, records) when length(asserted_maps) != length(records), do: false
def assert_lists_match([subset_map | rest_of_assertion], records) do
records
|> Enum.with_index()
|> Enum.any?(fn {record, index} ->
matches_subset?(subset_map, record) &&
assert_lists_match(rest_of_assertion, List.delete_at(records, index))
end)
end
defp matches_subset?(subset_map, record) do
Enum.all?(subset_map, fn {asserted_key, asserted_value} ->
Map.get(record, asserted_key) == asserted_value
end)
end
My tests:
defmodule MyApp.TestHelpersTest do
use ExUnit.Case, async: true
alias MyApp.TestHelpers
defmodule TestStruct do
defstruct [:name, :city]
end
test "empty lists" do
assert TestHelpers.assert_lists_match([], [])
# Expected to have one element
refute TestHelpers.assert_lists_match([%{name: "bar"}], [])
# Expected to be empty
refute TestHelpers.assert_lists_match([], [%{name: "bar"}])
refute TestHelpers.assert_lists_match([], [%TestStruct{name: "bar"}])
end
test "single matches" do
assert TestHelpers.assert_lists_match([%{name: "bar"}], [%{name: "bar"}])
assert TestHelpers.assert_lists_match([%{name: "bar"}], [%TestStruct{name: "bar"}])
assert TestHelpers.assert_lists_match([%{name: "bar"}], [%{name: "bar", city: 1}])
assert TestHelpers.assert_lists_match([%{name: "bar"}], [%TestStruct{name: "bar", city: 1}])
# Wrong value
refute TestHelpers.assert_lists_match([%{name: "bar"}], [%{name: "biz", city: 1}])
refute TestHelpers.assert_lists_match([%{name: "bar"}], [%TestStruct{name: "biz", city: 1}])
# Wrong key
refute TestHelpers.assert_lists_match([%{name: "bar"}], [%{biz: "bar", city: 1}])
# Missing key
refute TestHelpers.assert_lists_match([%{name: "bar"}], [%{city: 1}])
refute TestHelpers.assert_lists_match([%{name: "bar"}], [%TestStruct{city: 1}])
end
test "multiple matches" do
values = [
%{name: "John"},
%{name: "Jane"},
%{name: "Johan"}
]
# Testing that this works, but this isn't the best way to use this function
assert TestHelpers.assert_lists_match([%{}, %{}, %{}], values)
assert TestHelpers.assert_lists_match([%{name: "John"}, %{}, %{}], values)
assert TestHelpers.assert_lists_match([%{}, %{name: "John"}, %{}], values)
assert TestHelpers.assert_lists_match([%{}, %{}, %{name: "John"}], values)
for _ <- 1..10 do
assert TestHelpers.assert_lists_match(Enum.shuffle([%{name: "Jane"}, %{name: "Johan"}, %{name: "John"}]), values)
end
subsets =
for i <- 1..200 do
%{name: Faker.Name.name(), city: Faker.Address.city()}
end
for _ <- 1..10 do
values =
subsets
|> Enum.map(fn record ->
record
|> Map.put(:id, Faker.UUID.v4())
|> Map.put(:country, Faker.Address.En.country())
end)
assert TestHelpers.assert_lists_match(Enum.shuffle(subsets), values)
end
end
end