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