Packages

Assertions tailored for use in pipelines. Common assertions extracted for easy reuse. Tabular tests.

Current section

Files

Jump to
flow_assertions lib define body_parts.ex
Raw

lib/define/body_parts.ex

defmodule FlowAssertions.Define.BodyParts do
import ExUnit.Assertions
alias ExUnit.AssertionError
import FlowAssertions.Define.Defchain
alias FlowAssertions.Messages
@moduledoc """
Functions helpful in the construction of a new assertion.
Mostly, they give you more control over what's shown in a failing test by letting
you set `ExUnit.AssertionError` values like `:left` and `:right`.
All such functions take a string first argument. That's shorthand
for setting the `:message` field.
"""
@doc """
Like `ExUnit.Assertions.flunk/1` but the second argument is used to set `AssertionError` keys.
```
elaborate_flunk("the value is wrong", left: value_to_check)
```
Warning: as far as I know, the structure of `ExUnit.AssertionError` is not
guaranteed to be stable.
See also `elaborate_assert/3`.
"""
def elaborate_flunk(message, opts) do
try do
flunk message
rescue
ex in AssertionError ->
annotated =
Enum.reduce(opts, ex, fn {k, v}, acc -> Map.put(acc, k, v) end)
reraise annotated, __STACKTRACE__
end
end
@doc """
Like `ExUnit.Assertions.assert/2` but the third argument is used to set `AssertionError` keys.
```
elaborate_assert(
left =~ right,
"Regular expression didn't match",
left: left, right: right)
```
Warning: as far as I know, the structure of `ExUnit.AssertionError` is not
guaranteed to be stable.
See also `elaborate_assert_equal/4`.
"""
defchain elaborate_assert(value, message, opts) do
if !value, do: elaborate_flunk(message, opts)
end
@doc """
`elaborate_assert/3`, except the value is expected to be falsy.
"""
defchain elaborate_refute(value, message, opts),
do: elaborate_assert(!value, message, opts)
@doc """
This replicates the diagnostic output from `assert a == b`, except for the
code snippet that's reported.
The user will see a failing test containing:
Assertion with == failed
code: assert_same_map(new, old, ignoring: [:stable])
left: ...
right: ...
... instead of the assertion that actually failed, something like this:
Assertion with == failed
code: assert Map.drop(new, fields_to_ignore) == Map.drop(old, fields_to_ignore)
left: ...
right: ...
"""
defchain elaborate_assert_equal(left, right) do
elaborate_assert(left == right,
Messages.stock_equality,
left: left, right: right,
expr: AssertionError.no_value)
end
# ----------------------------------------------------------------------------
@doc """
Flunk test if it checks structure fields that don't exist.
It doesn't make sense to write an assertion that checks a field that
a structure can't contain. If a user tries, this function will object with a message like:
```
Test error: there is no key `:b` in a `MyApp.Struct`
```
Notes:
* It's safe to call on non-struct values.
* It returns its first argument.
"""
defchain struct_must_have_key!(struct, key) when is_struct(struct) do
elaborate_assert(
Map.has_key?(struct, key),
Messages.required_key_missing(key, struct),
left: struct |> Map.from_struct |> Map.keys)
end
def struct_must_have_key!(x, _), do: x
@doc"""
Same as `struct_must_have_key!/2` but checks multiple keys.
"""
defchain struct_must_have_keys!(struct, keys) when is_struct(struct) do
for key <- keys, do: struct_must_have_key!(struct, key)
end
def struct_must_have_keys!(x, _), do: x
# ----------------------------------------------------------------------------
@doc ~S"""
Run a function, perhaps generating an assertion error. If so, use the
keyword arguments to replace values in the error.
adjust_assertion_error(fn ->
MiscA.assert_good_enough(Map.get(kvs, key), expected)
end,
message: "Field `#{inspect key}` has the wrong value",
expr: AssertionError.no_value)
Setting the `expr` field to `AssertionError.no_value` has the handy effect of
making the reporting machinery report the code of the assertion the user called,
rather than the assertion that generated the error.
See also `adjust_assertion_message/2`
"""
def adjust_assertion_error(f, replacements) do
try do
f.()
rescue
ex in AssertionError ->
Enum.reduce(replacements, ex, fn {key, value}, acc ->
Map.put(acc, key, value)
end)
|> reraise(__STACKTRACE__)
end
end
@doc ~S"""
Run a function, perhaps generating an assertion error. If so, call the second function, passing the current assertion message as its argument. The result is installed as the new assertion message.
keyword arguments to replace values in the error.
adjust_assertion_error(fn ->
MiscA.assert_good_enough(Map.get(kvs, key), expected)
end,
message: "Field `#{inspect key}` has the wrong value",
expr: AssertionError.no_value)
Setting the `expr` field to `AssertionError.no_value` has the handy effect of
making the reporting machinery report the code of the assertion the user called,
rather than the assertion that generated the error.
See also `adjust_assertion_error/2`"
"""
def adjust_assertion_message(asserter, adjuster) do
try do
asserter.()
rescue
ex in AssertionError ->
Map.put(ex, :message, adjuster.(ex.message))
|> reraise(__STACKTRACE__)
end
end
end