Skip to content

Commit 854e53c

Browse files
committed
Add additional test assertions
1 parent a8f15ac commit 854e53c

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed

lib/together/assertions.ex

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
defmodule Together.Assertions do
2+
@moduledoc """
3+
Helpers for easy-to-read tests
4+
5+
This module makes heavy use of macros to preserve nice error messages.
6+
"""
7+
8+
@doc """
9+
Assert two values are equal
10+
11+
## Example
12+
13+
my_function()
14+
|> assert_equal(2)
15+
16+
"""
17+
defmacro assert_equal(left, right) do
18+
quote do
19+
left = unquote(left)
20+
assert left == unquote(right)
21+
left
22+
end
23+
end
24+
25+
@doc """
26+
Assert the first argument matches the given pattern
27+
28+
## Example
29+
30+
my_function()
31+
|> assert_match(%{"key" => _})
32+
33+
"""
34+
defmacro assert_match(left, right) do
35+
quote do
36+
left = unquote(left)
37+
assert unquote(right) = left
38+
left
39+
end
40+
end
41+
42+
@doc """
43+
Assert a timestamp is recent
44+
45+
Defaults to a 10 second threshold.
46+
47+
## Example
48+
49+
value.last_accessed_at
50+
|> assert_recent()
51+
52+
"""
53+
defmacro assert_recent(value, threshold \\ 10) do
54+
quote do
55+
assert DateTime.diff(DateTime.utc_now(), unquote(value), :second) <= unquote(threshold)
56+
end
57+
end
58+
59+
@doc """
60+
Assert two lists are equivalent (in any order)
61+
62+
This macro uses MapSet to compare two enumerable values in an order-unspecific way. This is not
63+
suitable for lists that may have duplicate elements.
64+
65+
## Example
66+
67+
my_function()
68+
|> assert_set_equal([2, 1])
69+
70+
"""
71+
defmacro assert_set_equal(left, right) do
72+
quote do
73+
assert MapSet.new(unquote(left)) == MapSet.new(unquote(right))
74+
end
75+
end
76+
77+
@doc """
78+
Assert two lists match (in any order)
79+
80+
Taken from https://elixirforum.com/t/assert-a-list-of-patterns-ignoring-order/46068/8.
81+
82+
## Example
83+
84+
my_function()
85+
|> assert_set_match([2, 1])
86+
87+
"""
88+
defmacro assert_set_match(expression, patterns) when is_list(patterns) do
89+
clauses =
90+
patterns
91+
|> Enum.with_index()
92+
|> Enum.flat_map(fn {pattern, index} ->
93+
quote generated: true do
94+
unquote(pattern) -> unquote(index)
95+
end
96+
end)
97+
|> Kernel.++(quote(generated: true, do: (_ -> :not_found)))
98+
99+
code = Macro.escape({:assert_set_match, [], [expression, patterns]}, prune_metadata: true)
100+
pins = collect_pins_from_pattern(patterns, Macro.Env.vars(__CALLER__))
101+
102+
quote generated: true do
103+
expression = unquote(expression)
104+
patterns = unquote(Macro.escape(patterns))
105+
fun = fn x -> case x, do: unquote(clauses) end
106+
pins = unquote(pins)
107+
108+
result =
109+
Enum.reduce(expression, %{}, fn item, acc ->
110+
case fun.(item) do
111+
:not_found ->
112+
raise ExUnit.AssertionError,
113+
expr: unquote(code),
114+
left: item,
115+
message: "Item does not match any pattern\n" <> ExUnit.Assertions.__pins__(pins)
116+
117+
index when is_map_key(acc, index) ->
118+
raise ExUnit.AssertionError,
119+
expr: unquote(code),
120+
left: [item, acc[index]],
121+
message: "Multiple items match pattern\n" <> ExUnit.Assertions.__pins__(pins)
122+
123+
index when is_integer(index) ->
124+
Map.put(acc, index, item)
125+
end
126+
end)
127+
128+
if map_size(result) == length(patterns) do
129+
:ok
130+
else
131+
raise ExUnit.AssertionError,
132+
expr: unquote(code),
133+
left: expression,
134+
message: "Expected set to have #{length(patterns)} entries, got: #{map_size(result)}\n"
135+
end
136+
end
137+
end
138+
139+
defp collect_pins_from_pattern(expr, vars) do
140+
{_, pins} =
141+
Macro.prewalk(expr, %{}, fn
142+
{:quote, _, [_]}, acc ->
143+
{:ok, acc}
144+
145+
{:quote, _, [_, _]}, acc ->
146+
{:ok, acc}
147+
148+
{:^, _, [var]}, acc ->
149+
identifier = var_context(var)
150+
151+
if identifier in vars do
152+
{:ok, Map.put(acc, var_context(var), var)}
153+
else
154+
{:ok, acc}
155+
end
156+
157+
form, acc ->
158+
{form, acc}
159+
end)
160+
161+
Enum.to_list(pins)
162+
end
163+
164+
defp var_context({name, meta, context}) do
165+
{name, meta[:counter] || context}
166+
end
167+
end

0 commit comments

Comments
 (0)