defmodule Assertions do @moduledoc """ Helpful functions to help you write better tests. """ @doc """ Tests if two lists have the same elements without asserting they are in the same order. When you compare two lists for equality using `==`, order matters, which means `assert [1,2,3] == [1,3,2]` fails. However, for many situations, the order of elements in a list doesn't matter. If you only care that two lists have exactly the same elements, but not what order those elements are in, this is the assertion for you! ## Examples iex> lists_equal?([1,2,3], [1,3,2]) true iex> lists_equal?([1,2,4], [1,3,2]) false iex> lists_equal?([1,2,3,4], [1,3,2]) false iex> lists_equal?([1,2,3], [1,3,2,4]) false """ def lists_equal?(left, right) do lists_equal?(left, right, &Kernel.==/2) end @doc """ Tests if two lists have the same elements according to a given comparison function without asserting they are in the same order. This is very similar to `lists_equal?/2`, but it allows you to determine how two elements in your list are considered equal. This is especially helpful when comparing lists of structs, since comparing structs for equality with `==` is very error prone. The comparison function you pass as the third argument must be a function that takes two elements (one from the left list and one from the right list) and returns a boolean if those two elements are equal. ## Examples iex> lists_equal?([1,2,3], [1,3,2], &Kernel.==/2) true iex> lists_equal?(["12", "23.5"], ["23", "12 "], fn left, right -> ...> {left_int, _} = Integer.parse(left) ...> {right_int, _} = Integer.parse(right) ...> left_int == right_int ...> end) true iex> left = [DateTime.utc_now(), Date.utc_today()] iex> right = [Date.utc_today(), DateTime.utc_now()] iex> lists_equal?(left, right, &structs_equal?(&1, &2, [:year, :month, :day])) true iex> left = [DateTime.utc_now()] iex> right = [Date.utc_today(), DateTime.utc_now()] iex> lists_equal?(left, right, &structs_equal?(&1, &2, [:year, :month, :day])) false iex> left = [Date.utc_today(), DateTime.utc_now()] iex> right = [DateTime.utc_now()] iex> lists_equal?(left, right, &structs_equal?(&1, &2, [:year, :month, :day])) false """ def lists_equal?(left, right, comparison) do compare(left, right, comparison) == [] and compare(right, left, comparison) == [] end defp compare(left, right, comparison) do Enum.reduce(left, right, fn left_element, list -> case Enum.find_index(list, &comparison.(left_element, &1)) do nil -> list index -> List.delete_at(list, index) end end) end @doc """ Tests if two structs have the same values at the given keys. Directly comparing structs for equality using `==` can be very tricky, which is why you almost never want to do that. Using this assertion, you get to determine what makes two structs equal while allowing for inconsequential differences. ## Examples iex> left = DateTime.utc_now() iex> right = Date.utc_today() iex> structs_equal?(left, right, [:year, :month, :day]) true iex> left = DateTime.utc_now() iex> right = Date.utc_today() iex> structs_equal?(left, right, [:year, :month, :day, :second]) false """ def structs_equal?(left, right, keys) do map_values_equal?(left, right, keys, strict: true) end @doc """ Tests if two maps have the same values at the given keys. ## Examples iex> left = %{first: :first, second: :second} iex> right = %{first: :first, second: :second, third: :third} iex> map_values_equal?(left, right, [:first, :second]) true iex> left = %{first: "first", second: :second} iex> right = %{first: :first, second: :second, third: :third} iex> map_values_equal?(left, right, [:first, :second]) false iex> left = %{first: :first, second: :second} iex> right = %{first: :first, third: :third} iex> map_values_equal?(left, right, [:first, :second]) false By default if both maps are missing a given key, they are not considered equal. iex> left = %{first: :first, second: :second} iex> right = %{first: :first, second: :second, third: :third} iex> map_values_equal?(left, right, [:first, :second, "not_there"]) false If you would like maps to be considered equal in this case, you can pass `strict: false` as the fourth argument. iex> left = %{first: :first, second: :second} iex> right = %{first: :first, second: :second, third: :third} iex> map_values_equal?(left, right, [:first, :second, "not_there"], strict: false) true """ def map_values_equal?(left, right, keys, options \\ [strict: true]) do if options[:strict] do keys_present?(left, right, keys) and values_equal?(left, right, keys) else values_equal?(left, right, keys) end end defp keys_present?(left, right, keys) do left_keys = Map.keys(left) right_keys = Map.keys(right) Enum.all?(keys, fn key -> key in left_keys and key in right_keys end) end defp values_equal?(left, right, keys) do Enum.all?(keys, fn key -> value_for(left, key) == value_for(right, key) end) end defp value_for(struct, key) do Map.get(struct, key, {:error, :key_not_found_for_assertion}) end @doc """ Tests that a message matching the given pattern, and only that message, is received within the given time period, specified in milliseconds. The optional second argument is a timeout for the `receive` to wait for the expected message, and defaults to 100ms. If you want to check that no message was received before the expected message, **and** that no message is received for a given time after calling `receive_only?/2`, you can combine `received_only?/2` with `ExUnit.Assertions.refute_receive/3`. assert receive_only?(:hello) refute_receive _, 100 ## Examples iex> send(self(), :hello) iex> receive_only?(:hello) true iex> send(self(), [:hello]) iex> receive_only?([_]) true iex> a = :hello iex> send(self(), :hello) iex> receive_only?(^a) true iex> send(self(), :hello) iex> send(self(), :hello_again) iex> receive_only?(:hello) false If a message is received after the function has matched a message to the given pattern, but the second message is received before the timeout, that second message is ignored and the function returns `true`. This function only tests that the message that matches the given pattern was the first message in the process inbox, and that nothing was sent between the sending the message that matches the pattern and when `receive_only?/2` was called. iex> Process.send_after(self(), :hello, 20) iex> Process.send_after(self(), :hello_again, 50) iex> receive_only?(:hello, 100) true iex> Process.send_after(self(), :hello, 50) iex> Process.send_after(self(), :hello_again, 20) iex> receive_only?(:hello, 100) false """ defmacro receive_only?(expected_pattern, timeout \\ 100) do pattern = Macro.expand(expected_pattern, __CALLER__) quote do receive do unquote(pattern) -> receive do _ -> false after 0 -> true end after unquote(timeout) -> false end end end @doc """ Tests that messages matching the given patterns, and only those messages, are received in order within the given time period, specified in milliseconds. This is an expansion of `receive_only?/2`. See the documentation there for details on the behavior of this function. ## Examples iex> send(self(), :hello) iex> send(self(), :hello_again) iex> send(self(), :goodbye) iex> receive_exactly?([:hello, :hello_again, :goodbye]) true iex> send(self(), :hello) iex> Process.send_after(self(), :hello_again, 50) iex> receive_exactly?([:hello, :hello_again]) true iex> send(self(), :hello_again) iex> send(self(), :hello) iex> receive_exactly?([:hello, :hello_again]) false iex> send(self(), :hello) iex> send(self(), :goodbye) iex> send(self(), :hello_again) iex> receive_exactly?([:hello, :hello_again]) false iex> send(self(), :hello) iex> send(self(), :hello_again) iex> send(self(), :goodbye) iex> receive_exactly?([:hello, :hello_again]) false iex> send(self(), :goodbye) iex> send(self(), :hello) iex> send(self(), :hello_again) iex> receive_exactly?([:hello, :hello_again]) false iex> hello = :hello iex> send(self(), hello) iex> send(self(), :hello_again) iex> receive_exactly?([^hello, :hello_again]) true iex> hello = :hello iex> send(self(), :hello_again) iex> send(self(), hello) iex> receive_exactly?([^hello, _]) false """ defmacro receive_exactly?(expected_patterns, timeout \\ 100) do caller = __CALLER__ [pattern | rest] = Enum.map(expected_patterns, &Macro.expand(&1, caller)) quote do receive do matched -> if match?(unquote(pattern), matched) do unquote(wait_for_pattern(rest)) else false end after unquote(timeout) -> false end end end defp wait_for_pattern([]) do quote do receive do _ -> false after 0 -> true end end end defp wait_for_pattern([{ignored, _, nil} | rest]) when is_atom(ignored) do quote do receive do _ -> unquote(wait_for_pattern(rest)) end end end defp wait_for_pattern([pattern | rest]) do quote do receive do matched -> if match?(unquote(pattern), matched) do unquote(wait_for_pattern(rest)) else false end end end end end