this repo has no description
1+++
2date = 2023-11-20
3title = "How do I write Elixir tests?"
4
5draft = true
6
7[taxonomies]
8tags = [
9 "elixir",
10 "testing",
11 "programming"
12]
13+++
14
15This post was created for myself to codify some basic guides that I use while
16writing tests. If you, my dear reader, read this then there is one important
17thing for you to remember:
18
19These are **guides** not *rules*. Each code base is different deviations and are
20expected and *will* happen. Just use the thing between your ears.
21
22## `@subject` module attribute for module under test
23
24While writing test in Elixir it is not always obvious what we are testing. Just
25imagine test like:
26
27```elixir
28test "foo should frobnicate when bar" do
29 bar = pick_bar()
30
31 assert :ok == MyBehaviour.foo(MyImplementation, bar)
32end
33```
34
35It is not obvious at the first sight what we are testing. And it is pretty
36simplified example, in real world it can became even harder to notice what is
37module under test (MUT).
38
39To resolve that I came up with a simple solution. I create module attribute
40named `@subject` that points to the MUT:
41
42```elixir
43@subject MyImplementation
44
45test "foo should frobnicate when bar" do
46 bar = pick_bar()
47
48 assert :ok == MyBehaviour.foo(@subject, bar)
49end
50```
51
52Now it is more obvious what is MUT and what is just wrapper code around it.
53
54In the past I have been using `alias` with `:as` option, like:
55
56```elixir
57alias MyImplementation, as: Subject
58```
59
60However I find module attribute to be more visually outstanding and make it
61easier for me to notice `@subject` than `Subject`. But your mileage may vary.
62
63## `describe` with function name
64
65That one is pretty basic, and I have seen that it is pretty standard for people:
66when you are writing tests for module functions, then group them in `describe`
67blocks that will contain name (and arity) of the function in the name. Example:
68
69```elixir
70# Module under test
71defmodule Foo do
72 def a(x, y, z) do
73 # some code
74 end
75end
76
77# Tests
78defmodule FooTest do
79 use ExUnit.Case, async: true
80
81 @subject Foo
82
83 describe "a/3" do
84 # Some tests here
85 end
86end
87```
88
89This allows me to see what functionality I am testing.
90
91Of course that doesn't apply to the Phoenix controllers, as there we do not test
92functions, but tuples in form `{method, path}` which I then write as `METHOD
93path`, for example `POST /users`.
94
95## Avoid module mocking
96
97In Elixir we have bunch of the mocking libraries out there, but most of them
98have quite substantial issue for me - these prevent me from using `async: true`
99for my tests. This often causes substantial performance hit, as it prevents
100different modules to run in parallel (not single tests, *modules*, but that is
101probably material for another post).
102
103Instead of mocks I prefer to utilise dependency injection. Some people may argue
104that "Elixir is FP, not OOP, there is no need for DI" and they cannot be further
105from truth. DI isn't related to OOP, it just have different form, called
106function arguments. For example, if we want to have function that do something
107with time, in particular - current time. Then instead of writing:
108
109```elixir
110def my_function(a, b) do
111 do_foo(a, b, DateTime.utc_now())
112end
113```
114
115Which would require me to use mocks for `DateTime` or other workarounds to make
116tests time-independent. I would do:
117
118```elixir
119def my_function(a, b, now \\ DateTime.utc_now()) do
120 do_foo(a, b, now)
121end
122```
123
124Which still provide me the ergonomics of `my_function/2` as above, but is way
125easier to test, as I can pass the date to the function itself. This allows me to
126run this test in parallel as it will not cause other tests to do weird stuff
127because of altered `DateTime` behaviour.
128
129## Avoid `ex_machina` factories
130
131I have poor experience with tools like `ex_machina` or similar. These often
132bring whole [Banana Gorilla Jungle problem][bgj] back, just changed a little, as
133now instead of just passing data around, we create all needless structures for
134sole purpose of test, even when they aren't needed for anything.
135
136[bgj]: https://softwareengineering.stackexchange.com/q/368797
137
138Start with example from [ExMachina README](https://github.com/beam-community/ex_machina#overview):
139
140```elixir
141defmodule MyApp.Factory do
142 # with Ecto
143 use ExMachina.Ecto, repo: MyApp.Repo
144
145 # without Ecto
146 use ExMachina
147
148 def user_factory do
149 %MyApp.User{
150 name: "Jane Smith",
151 email: sequence(:email, &"email-#{&1}@example.com"),
152 role: sequence(:role, ["admin", "user", "other"]),
153 }
154 end
155
156 def article_factory do
157 title = sequence(:title, &"Use ExMachina! (Part #{&1})")
158 # derived attribute
159 slug = MyApp.Article.title_to_slug(title)
160 %MyApp.Article{
161 title: title,
162 slug: slug,
163 # associations are inserted when you call `insert`
164 author: build(:user),
165 }
166 end
167
168 # derived factory
169 def featured_article_factory do
170 struct!(
171 article_factory(),
172 %{
173 featured: true,
174 }
175 )
176 end
177
178 def comment_factory do
179 %MyApp.Comment{
180 text: "It's great!",
181 article: build(:article),
182 author: build(:user) # That line is added by me
183 }
184 end
185end
186```
187
188For start we can see a single problem there - we do not validate our factories
189against our schema changesets. Without additional tests like:
190
191```elixir
192@subject MyApp.Article
193
194test "factory conforms to changeset" do
195 changeset = @subject.changeset(%@subject{}, params_for(:article))
196
197 assert changeset.valid?
198end
199```
200
201We cannot be sure that our tests test what we want them to test. And if we pass
202custom attribute values in some tests it gets even worse, because we cannot be
203sure if these are conforming either.
204
205That mean that our tests may be moot, because we aren't testing against real
206situations, but against some predefined state.
207
208Another problem is that if we need to alter the behaviour of the factory it can
209became quite convoluted. Imagine situation when we want to test if comments by
210author of the post have some special behaviour (for example it has some
211additional CSS class to be able to mark them in CSS). That require from us to do
212some dancing around passing custom attributes:
213
214```elixir
215test "comments by author are special" do
216 post = insert(:post)
217 comment = insert(:comment, post: post, author: post.author)
218
219 # rest of the test
220end
221```
222
223And this is simplified example. In the past I needed to deal with situations
224where I was creating a lot of data to pass through custom attributes to make
225test sensible.
226
227Instead I prefer to do stuff directly in code. Instead of relying on some
228"magical" functions provided by some "magical" macros from external library I
229can use what I already have - functions in my application.
230
231Instead of:
232
233```elixir
234test "comments by author are special" do
235 post = insert(:post)
236 comment = insert(:comment, post: post, author: post.author)
237
238 # rest of the test
239end
240```
241
242Write:
243
244```elixir
245test "comments by author are special" do
246 author = MyApp.Users.create(%{
247 name: "John Doe",
248 email: "john@example.com"
249 })
250 post = MyApp.Blog.create_article(%{
251 author: author,
252 content: "Foo bar",
253 title: "Foo bar"
254 })
255 comment = MyApp.Blog.create_comment_for(article, %{
256 author: author,
257 content: "Foo bar"
258 })
259
260 # rest of the test
261end
262```
263
264It may be a little bit more verbose, but it makes tests way more readable in my
265opinion. You have all details just in place and you know what to expect. And if
266you need some piece of data in all (or almost all) tests within
267module/`describe` block, then you can always can use `setup/1` blocks. Or you
268can create function per module that will generate data for you. As long as your
269test module is self-contained and do not receive "magical" data out of thin air,
270it is ok for me. But `ex_machina` is, in my opinion, terrible idea brought from
271Rails world, that make little to no sense in Elixir.
272
273If you really need such factories, then just write your own functions that will
274use your contexts instead of relying on another library. For example:
275
276```elixir
277import ExUnit.Assertions
278
279def create_user(name, email \\ nil, attrs \\ %{}) do
280 email = email || "#{String.replace(name, " ", ".")}@example.com"
281 attrs = Map.merge(attrs, %{name: name, email: email})
282
283 assert {:ok, user} = MyApp.Users.create(attrs)
284
285 user
286end
287
288# And so on…
289```
290
291This way you do not need to check if all tests use correct validations any
292longer, as your system will do that for you.