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.