# `Moar.Assertions`
[🔗](https://github.com/synchronal/moar/blob/main/lib/assertions.ex#L1)

ExUnit assertions.

See also: [siiibo/assert_match](https://github.com/siiibo/assert_match) which is similar to this module's
`assert_eq` function but with pattern matching.

# `assert_eq_opt_shortcut`

```elixir
@type assert_eq_opt_shortcut() :: :downcase | :sort | :squish | :to_string | :trim
```

# `assert_eq_opts`

```elixir
@type assert_eq_opts() ::
  (any() -&gt; any())
  | {:apply, (any() -&gt; any()) | assert_eq_opt_shortcut()}
  | assert_eq_opt_shortcut()
  | {:except, list()}
  | :ignore_order
  | {:ignore_order, boolean()}
  | {:ignore_whitespace, :leading_and_trailing}
  | {:map, (any() -&gt; any()) | assert_eq_opt_shortcut()}
  | {:only, list()}
  | {:returning, any()}
  | {:whitespace, :squish | :trim}
  | {:within, number() | {number(), Moar.Duration.time_unit()}}
```

# `assert_contains`

```elixir
@spec assert_contains(map(), map()) :: map()
@spec assert_contains(list(), list() | any()) :: list()
```

Asserts that the `left` list or map contains all of the items in the `right` list or map,
or contains the single `right` element if it's not a list or map. Returns `left` or raises `ExUnit.AssertionError`.

```elixir
iex> assert_contains([1, 2, 3], [1, 3])
[1, 2, 3]

iex> assert_contains(%{a: 1, b: 2, c: 3}, %{a: 1, c: 3})
%{a: 1, b: 2, c: 3}

iex> assert_contains([1, 2, 3], 2)
[1, 2, 3]
```

# `assert_eq`

```elixir
@spec assert_eq(
  left :: any(),
  right :: any(),
  opts :: assert_eq_opts() | [assert_eq_opts()]
) :: any()
```

Asserts that the `left` and `right` values are equal. It returns the `left` value unless the assertion fails,
or unless the `:returning` option is used. As a special case, if the first argument is a string and the second
is a Regex, the comparison is done with the `=~` operator instead of the `==` operator.

_Style note: the authors prefer to use `assert` in most cases, using `assert_eq` only when the extra options
are helpful or when they want to make assertions in a pipeline._

```elixir
iex> import Moar.Assertions

# prefer regular `assert` when `assert_eq` is not necessary
iex> assert Map.put(%{a: 1}, :b, 2) == %{a: 1, b: 2}
true

# works nicely with pipes
iex> %{a: 1} |> Map.put(:b, 2) |> assert_eq(%{a: 1, b: 2})
%{a: 1, b: 2}

# returns an arbitrary value instead of the left value
iex> map = %{a: 1, b: 2}
iex> map |> Map.get(:a) |> assert_eq(1, returning: map)
%{a: 1, b: 2}

# compares using a regex if `left` is a string and `right` is a Regex
iex> assert_eq "the length is 10cm", ~r/.*length is \d{2}cm/
```

`left` and `right` can be transformed by passing in one or more functions (optionally using the `apply:` keyword).
If `left` and `right` are lists, all their values can be transformed using the `map:` option.

```elixir
iex> import Moar.Assertions

# apply a function
iex> assert_eq("hello", "heLLo", &String.downcase/1)
"hello"

# apply multiple functions (left to right)
iex> assert_eq("hello", "heLLo", [&String.downcase/1, &String.reverse/1])
"olleh"

# apply a function explicitly
iex> assert_eq("hello", "heLLo", apply: &String.downcase/1)
"hello"

# map all elements with a function
iex> assert_eq(["a", "B"], ["A", "b"], map: &String.downcase/1)
["a", "b"]

# combine apply and map (left to right)
iex> assert_eq(["a", "B", "C"], ["A", "b", "Z"], apply: &Enum.drop(&1, -1), map: &String.downcase/1)
["a", "b"]
```

If `left` and `right` are maps, `only: <list>` will only consider the given keys, and `except: <list>` will ignore
certain keys.

```elixir
iex> import Moar.Assertions

# ignore one or more keys of a map
iex> assert_eq(%{a: 1, b: 2, c: 3}, %{a: 1, b: 100, c: 3}, except: [:b])
%{a: 1, b: 2, c: 3}

# only assert on one or more keys of a map
iex> assert_eq(%{a: 1, b: 2, c: 3}, %{a: 1, b: 100, c: 3}, only: [:a, :c])
%{a: 1, b: 2, c: 3}
```

If `left` and `right` are lists, their order can be ignored using `:ignore_order`.

```elixir
iex> import Moar.Assertions

# ignoring order when comparing lists
iex> assert_eq(["a", "b"], ["b", "a"], :ignore_order)
["a", "b"]
```

For inexact numerical assertions, `within: <delta>` works like `ExUnit.Assertions.assert_in_delta/4`, and
for inexact date and datetime assertions, `within: {<delta>, <time-unit>}` asserts that `left` and `right`
are within some duration of each other. See `Moar.Duration` for more about time units, and also see
`Moar.Assertions.assert_recent/2`. If `left` and `right` are strings, they are parsed as ISO8601 dates.

```elixir
iex> import Moar.Assertions

# assert within a delta (this particular case could use ExUnit.Assertions.assert_in_delta/4).
iex> assert_eq(4/28, 0.14, within: 0.01)
0.14285714285714285

# assert within a time delta
iex> inserted_at = ~U[2022-01-02 03:00:00Z]
iex> updated_at = ~U[2022-01-02 03:04:00Z]
iex> assert_eq(inserted_at, updated_at, within: {10, :minute})
~U[2022-01-02 03:00:00Z]

# assert within a time delta when inputs are time strings
iex> inserted_at = "2022-01-02T03:00:00Z"
iex> updated_at = "2022-01-02T03:04:00Z"
iex> assert_eq(inserted_at, updated_at, within: {10, :minute})
"2022-01-02T03:00:00Z"
```

The following transformation shortcuts are supported:
* `:downcase` - if `left` and `right` are strings, converts them to lowercase before comparing
* `:sort` - the same as `:ignore_order`
* `:squish` - if `left` and `right` are strings, trims the strings and replaces consecutive whitespace characters
  with a single space using `Moar.String.squish/1` before comparing
* `:to_string` - calls `String.Chars.to_string` on `left` and `right` before comparing
* `:trim` - if `left` and `right` are strings, trims the strings using `String.trim/1` before comparing

```elixir
iex> import Moar.Assertions

iex> assert_eq("FOO bar", "foo bar", :downcase)
"foo bar"

iex> assert_eq(" FOO   bar  ", "foo bar", [:downcase, :squish])
"foo bar"
```

The following options are deprecated:
* `ignore_order: <boolean>` - use `:ignore_order` instead of `ignore_order: true`
* `ignore_whitespace: :leading_and_trailing` - use `:squish` or `:trim` instead
* `whitespace: :squish` - use `:squish` instead
* `whitespace: :trim` - use `:trim` instead

# `assert_recent`

```elixir
@spec assert_recent(DateTime.t() | NaiveDateTime.t() | binary(), Moar.Duration.t()) ::
  DateTime.t() | NaiveDateTime.t() | binary()
```

Asserts that `datetime` is within `recency` of now (in UTC), returning `datetime` if the assertion succeeeds.

Uses `assert_eq(datetime, now, within: recency)` under the hood.

* `datetime` can be a `DateTime`, a `NaiveDateTime`, or an ISO8601-formatted UTC datetime string.
* `recency` is a `Moar.Duration` and defaults to `{10, :second}`.

```elixir
iex> five_seconds_ago = Moar.DateTime.add(DateTime.utc_now(), {-5, :second})
iex> assert_recent five_seconds_ago

iex> twenty_seconds_ago = Moar.DateTime.add(DateTime.utc_now(), {-20, :second})
iex> assert_recent twenty_seconds_ago, {25, :second}

```

# `assert_that`
*macro* 

```elixir
@spec assert_that(any(), changes: any(), from: any(), to: any()) :: Macro.t()
```

Asserts that a pre-condition and a post-condition are true after performing an action,
returning the result of the action.

To use an anonymous function as the action, wrap it in parentheses and call it with `.()`.

## Examples

```elixir
iex> {:ok, agent} = Agent.start(fn -> 0 end)
...>
iex> assert_that Agent.update(agent, fn s -> s + 1 end),
...>     changes: Agent.get(agent, fn s -> s end),
...>     from: 0,
...>     to: 1
:ok
...>
iex> assert_that Agent.update(agent, fn s -> s + 1 end),
...>     changes: Agent.get(agent, fn s -> s end),
...>     to: 2
:ok
...>
iex> assert_that (fn -> Agent.update(agent, fn s -> s + 1 end) end).(),
...>     changes: Agent.get(agent, fn s -> s end),
...>     to: 3
:ok
```

# `refute_that`
*macro* 

```elixir
@spec refute_that(any(), [{:changes, any()}]) :: Macro.t()
```

Refute that a condition is changed after performing an action, returning the result of the action.

## Examples

```elixir
iex> {:ok, agent} = Agent.start(fn -> 0 end)
...>
iex> refute_that Function.identity(1),
...>     changes: Agent.get(agent, fn s -> s end)
1

iex> refute_that Function.identity(5),
...>     changes: %{a: 1}
5
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
