Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

assert_maps_equal should have a default third argument #30

Open
fabio-t opened this issue Oct 5, 2023 · 2 comments
Open

assert_maps_equal should have a default third argument #30

fabio-t opened this issue Oct 5, 2023 · 2 comments

Comments

@fabio-t
Copy link

fabio-t commented Oct 5, 2023

I think this

defmacro assert_maps_equal(left, right, keys_or_comparison) do
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?

@cheerfulstoic
Copy link
Contributor

cheerfulstoic commented Dec 17, 2023

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

@cheerfulstoic
Copy link
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants