Assertions
Installation
Add assertions
to your list of dependencies in mix.exs
:
def deps do
[{:assertions, "~> 0.10", only: :test}]
end
Usage
Importing
If you only want some assertions in a given module, then import just the
functions that you want to use. Otherwise you can simply call import Assertions
and all assertions will be directly available to your test code.
def UsersTest do
use ExUnit.Case, async: true
require Assertions
import Assertions, only: [assert_lists_equal: 2]
# ...
end
Because these assertions are all macros, Assertions
must be require
d first
if you want to call a function like Assertions.assert_map_in_list/3
.
Importing assertions in an existing test case (like MyApp.DataCase
in a
Phoenix application) is typically recommended.
Here is an example of how you'd add assertions to your MyApp.DataCase
:
defmodule MayApp.DataCase do
use ExUnit.CaseTemplate
using do
quote do
import Ecto
import Ecto.Changeset
import Ecto.Query
import MyApp.DataCase
# Add the following line
import Assertions
end
end
end
Assertions.Case
If you want to have all assertions available to you by default, you can use the
provided Assertions.Case
macro. This is a very small wrapper around
ExUnit.Case
, and imports all assertions for your use.
def MyApp.UserTest do
use Assertions.Case, async: true
# ...
end
But importing assertions in an existing test case (like MyApp.DataCase
in a
Phoenix application) is typically recommended.
assertions
?
Why use There are three things this library offers:
- Concise, expressive assertions for common types of tests
- Flexibility through composition
- Exceptional error messages
Let's look at examples of all of these points. Let's say you have the following test:
defmodule UsersTest do
use ExUnit.Case, async: true
describe "update_all/2" do
test "updates the given users in the database and returns those updated users" do
alice = Factory.insert(:user, name: "Alice")
bob = Factory.insert(:user, name: "Bob")
updated_names =
[{alice, %{name: "Alice A."}, {bob, %{name: "Bob B."}}}]
|> Users.update_all()
|> Enum.map(& &1.name)
all_user_names =
User
|> Repo.all()
|> Enum.map(& &1.name)
Enum.each(["Alice A.", "Bob B."], fn name ->
assert name in updated_names
assert name in all_user_names
end
end
end
end
Testing elements in lists is a very common thing to do, but it's also very tricky! If you want to test those lists, you can't assert that they're equal because order matters with lists. Also, with those structs, you can't compare them directly because maybe there are associations that might be loaded in one struct but not loaded in the other. The above test is the best you can do to accurately test those changes with the standard testing tools.
Expressive assertions
But, with assertions
, you can write that test like this:
defmodule UsersTest do
use ExUnit.Case, async: true
import Assertions, only: [assert_lists_equal: 2]
describe "update_all/2" do
test "updates the given users in the database and returns those updated users" do
alice = Factory.insert(:user, name: "Alice")
bob = Factory.insert(:user, name: "Bob")
result = Users.update_all([{alice, %{name: "Alice A."}, {bob, %{name: "Bob B."}}}
result
|> Enum.map(& &1.name)
|> assert_lists_equal(["Alice A.", "Bob B."])
assert_lists_equal(result, Users.list_all(), &assert_structs_equal(&1, &2, [:name]))
end
end
end
assert_lists_equal
asserts that the two lists are equal without taking order
into account, which is most often the assertion that we want to make when
comparing lists.
Flexibility through composition
But assert_lists_equal
also solves the other problem we had when we wanted to
compare lists of structs. That second assertion:
assert_lists_equal(result, Users.list_all(), &assert_structs_equal(&1, &2, [:name]))
is comparing that the two lists are equal, but we give it a custom comparison
function. If we were to just use assert_lists_equal(results, Users.list_all())
,
then all values for all keys in those structs must be equal for them to be
considered equal, and this can be very error prone, especially when dealing with
structs used as Ecto resources that can have associations that are either loaded
or not loaded.
This ability to compose behavior of assertions lets you easily customize your assertions for each of your tests.
Exceptional error messages
assertions
always tries to give you the most helpful error messages possible
for any test failures to make it easy to see what went wrong and how to fix it.
Lets look at a different test:
test "a map in a list matches the value of this other map" do
map = %{key: :value, stores: :are, really: :helpful}
list = [
%{big: :map, with: :lots, of: :keys},
%{another: :big, store: :with, key: :values}
]
assert Enum.any?(list, fn map_in_list ->
Map.get(map_in_list, :key) == map.key
end)
end
The output you get from that failure looks like this:
Expected truthy, got false
code: assert Enum.any?(list, fn map_in_list -> Map.get(map_in_list, :key) == map.key() end)
arguments:
# 1
[
%{big: :map, of: :keys, with: :lots},
%{another: :big, key: :values, store: :with}
]
# 2
#Function<21.25555998/1 in Assertions.FailureExamples."test example"/1>
stacktrace:
test/failure_examples.exs:207: (test)
That's not really helpful. What we wanted to know was essentially "is a map with
the same values for a certain key in this list?", and we got no help in finding
what went wrong. With assertions
we can write that same assertion like this:
test "a map in a list matches the value of this other map" do
map = %{key: :value, stores: :are, really: :helpful}
list = [
%{big: :map, with: :lots, of: :keys},
%{another: :big, store: :with, key: :values}
]
assert_map_in_list(map, list, [:key])
end
And then the output looks like this:
Map matching the values for keys `:key` not found
code: assert_map_in_list(map, list, [:key])
arguments:
# 1
%{key: :value, really: :helpful, stores: :are}
# 2
[
%{big: :map, of: :keys, with: :lots},
%{another: :big, key: :values, store: :with}
]
left: %{key: :value}
right: [%{}, %{key: :values}]
stacktrace:
test/failure_examples.exs:206: (test)
The error message there in the left
and right
keys shrinks down the output
to only show us the relevant information. I can see here that one map didn't
contain the key we wanted, and the value was different in the other.