···
3
+
title = "How do I write Elixir tests?"
13
+
This post was created for myself to codify some basic guides that I use while
14
+
writing tests. If you, my dear reader, read this then there is one important
15
+
thing for you to remember:
17
+
These are **guides** not *rules*. Each code base is different deviations and are
18
+
expected and *will* happen. Just use the thing between your ears.
20
+
## `@subject` module attribute for module under test
22
+
While writing test in Elixir it is not always obvious what we are testing. Just
26
+
test "foo should frobnicate when bar" do
29
+
assert :ok == MyBehaviour.foo(MyImplementation, bar)
33
+
It is not obvious at the first sight what we are testing. And it is pretty
34
+
simplified example, in real world it can became even harder to notice what is
35
+
module under test (MUT).
37
+
To resolve that I came up with a simple solution. I create module attribute
38
+
named `@subject` that points to the MUT:
41
+
@subject MyImplementation
43
+
test "foo should frobnicate when bar" do
46
+
assert :ok == MyBehaviour.foo(@subject, bar)
50
+
Now it is more obvious what is MUT and what is just wrapper code around it.
52
+
In the past I have been using `alias` with `:as` option, like:
55
+
alias MyImplementation, as: Subject
58
+
However I find module attribute to be more visually outstanding and make it
59
+
easier for me to notice `@subject` than `Subject`. But your mileage may vary.
61
+
## `describe` with function name
63
+
That one is pretty basic, and I have seen that it is pretty standard for people:
64
+
when you are writing tests for module functions, then group them in `describe`
65
+
blocks that will contain name (and arity) of the function in the name. Example:
76
+
defmodule FooTest do
77
+
use ExUnit.Case, async: true
87
+
This allows me to see what functionality I am testing.
89
+
Of course that doesn't apply to the Phoenix controllers, as there we do not test
90
+
functions, but tuples in form `{method, path}` which I then write as `METHOD
91
+
path`, for example `POST /users`.
93
+
## Avoid module mocking
95
+
In Elixir we have bunch of the mocking libraries out there, but most of them
96
+
have quite substantial issue for me - these prevent me from using `async: true`
97
+
for my tests. This often causes substantial performance hit, as it prevents
98
+
different modules to run in parallel (not single tests, *modules*, but that is
99
+
probably material for another post).
101
+
Instead of mocks I prefer to utilise dependency injection. Some people may argue
102
+
that "Elixir is FP, not OOP, there is no need for DI" and they cannot be further
103
+
from truth. DI isn't related to OOP, it just have different form, called
104
+
function arguments. For example, if we want to have function that do something
105
+
with time, in particular - current time. Then instead of writing:
108
+
def my_function(a, b) do
109
+
do_foo(a, b, DateTime.utc_now())
113
+
Which would require me to use mocks for `DateTime` or other workarounds to make
114
+
tests time-independent. I would do:
117
+
def my_function(a, b, now \\ DateTime.utc_now()) do
122
+
Which still provide me the ergonomics of `my_function/2` as above, but is way
123
+
easier to test, as I can pass the date to the function itself. This allows me to
124
+
run this test in parallel as it will not cause other tests to do weird stuff
125
+
because of altered `DateTime` behaviour.
127
+
## Avoid `ex_machina` factories
129
+
I have poor experience with tools like `ex_machina` or similar. These often
130
+
bring whole [Banana Gorilla Jungle problem][bgj] back, just changed a little, as
131
+
now instead of just passing data around, we create all needless structures for
132
+
sole purpose of test, even when they aren't needed for anything.
134
+
[bgj]: https://softwareengineering.stackexchange.com/q/368797
136
+
Start with example from [ExMachina README](https://github.com/beam-community/ex_machina#overview):
139
+
defmodule MyApp.Factory do
141
+
use ExMachina.Ecto, repo: MyApp.Repo
146
+
def user_factory do
148
+
name: "Jane Smith",
149
+
email: sequence(:email, &"email-#{&1}@example.com"),
150
+
role: sequence(:role, ["admin", "user", "other"]),
154
+
def article_factory do
155
+
title = sequence(:title, &"Use ExMachina! (Part #{&1})")
156
+
# derived attribute
157
+
slug = MyApp.Article.title_to_slug(title)
161
+
# associations are inserted when you call `insert`
162
+
author: build(:user),
167
+
def featured_article_factory do
176
+
def comment_factory do
178
+
text: "It's great!",
179
+
article: build(:article),
180
+
author: build(:user) # That line is added by me
186
+
For start we can see a single problem there - we do not validate our factories
187
+
against our schema changesets. Without additional tests like:
190
+
@subject MyApp.Article
192
+
test "factory conforms to changeset" do
193
+
changeset = @subject.changeset(%@subject{}, params_for(:article))
195
+
assert changeset.valid?
199
+
We cannot be sure that our tests test what we want them to test. And if we pass
200
+
custom attribute values in some tests it gets even worse, because we cannot be
201
+
sure if these are conforming either.
203
+
That mean that our tests may be moot, because we aren't testing against real
204
+
situations, but against some predefined state.
206
+
Another problem is that if we need to alter the behaviour of the factory it can
207
+
became quite convoluted. Imagine situation when we want to test if comments by
208
+
author of the post have some special behaviour (for example it has some
209
+
additional CSS class to be able to mark them in CSS). That require from us to do
210
+
some dancing around passing custom attributes:
213
+
test "comments by author are special" do
214
+
post = insert(:post)
215
+
comment = insert(:comment, post: post, author: post.author)
221
+
And this is simplified example. In the past I needed to deal with situations
222
+
where I was creating a lot of data to pass through custom attributes to make
225
+
Instead I prefer to do stuff directly in code. Instead of relying on some
226
+
"magical" functions provided by some "magical" macros from external library I
227
+
can use what I already have - functions in my application.
232
+
test "comments by author are special" do
233
+
post = insert(:post)
234
+
comment = insert(:comment, post: post, author: post.author)
243
+
test "comments by author are special" do
244
+
author = MyApp.Users.create(%{
246
+
email: "john@example.com"
248
+
post = MyApp.Blog.create_article(%{
250
+
content: "Foo bar",
253
+
comment = MyApp.Blog.create_comment_for(article, %{
262
+
It may be a little bit more verbose, but it makes tests way more readable in my
263
+
opinion. You have all details just in place and you know what to expect. And if
264
+
you need some piece of data in all (or almost all) tests within
265
+
module/`describe` block, then you can always can use `setup/1` blocks. Or you
266
+
can create function per module that will generate data for you. As long as your
267
+
test module is self-contained and do not receive "magical" data out of thin air,
268
+
it is ok for me. But `ex_machina` is, in my opinion, terrible idea brought from
269
+
Rails world, that make little to no sense in Elixir.
271
+
If you really need such factories, then just write your own functions that will
272
+
use your contexts instead of relying on another library. For example:
275
+
import ExUnit.Assertions
277
+
def create_user(name, email \\ nil, attrs \\ %{}) do
278
+
email = email || "#{String.replace(name, " ", ".")}@example.com"
279
+
attrs = Map.merge(attrs, %{name: name, email: email})
281
+
assert {:ok, user} = MyApp.Users.create(attrs)
289
+
This way you do not need to check if all tests use correct validations any
290
+
longer, as your system will do that for you.