From afef848d724696b6f1acca55a2a5969fe718bc8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Niemier?= <~@hauleth.dev> Date: Fri, 1 May 2026 18:20:13 +0200 Subject: [PATCH] Furthermore, I think that mocking must be destroyed --- content/post/writing-tests.md | 125 ++++++++++++++++++++++++++-------- 1 file changed, 95 insertions(+), 30 deletions(-) diff --git a/content/post/writing-tests.md b/content/post/writing-tests.md index 1d129c9..1ad137a 100644 --- a/content/post/writing-tests.md +++ b/content/post/writing-tests.md @@ -1,8 +1,13 @@ +++ -date = 2023-11-20 +date = 2026-05-13 title = "How do I write Elixir tests?" -draft = true +description = """ +Personal guides for writing tests that are readable and maintainable. Stuff to +use, stuff to avoid, and how to organize stuff. + +Furthermore, I think that mocking must be destroyed. +""" [taxonomies] tags = [ @@ -11,17 +16,17 @@ tags = [ ] +++ -This post was created for myself to codify some basic guides that I use while -writing tests. If you, my dear reader, read this then there is one important -thing for you to remember: +I created this post for myself to codify some basic guides that I use while +writing tests. If you, my dear reader, want to read this, then remember one +important thing: -These are **guides** not *rules*. Each code base is different deviations and are -expected and *will* happen. Just use the thing between your ears. +These are **guides** not *rules*. Each codebase is different and exceptions are +expected and *will* happen. Just use the thing between your ears in your coding. ## `@subject` module attribute for module under test -While writing test in Elixir it is not always obvious what we are testing. Just -imagine test like: +While reading ExUnit test, I often find it hard to remember which of the used +modules is tested. Imagine test like: ```elixir test "foo should frobnicate when bar" do @@ -31,8 +36,8 @@ test "foo should frobnicate when bar" do end ``` -It is not obvious at the first sight what we are testing. And it is pretty -simplified example, in real world it can became even harder to notice what is +It is not obvious at the first sight what is tested here. And this is pretty +simplified example. In real world it can became even harder to notice what is module under test (MUT). To resolve that I came up with a simple solution. I create module attribute @@ -56,12 +61,12 @@ In the past I have been using `alias` with `:as` option, like: alias MyImplementation, as: Subject ``` -However I find module attribute to be more visually outstanding and make it +However, I find module attribute to be more visually distinctive, which make it easier for me to notice `@subject` than `Subject`. But your mileage may vary. ## `describe` with function name -That one is pretty basic, and I have seen that it is pretty standard for people: +That one is pretty basic. I have seen that it is pretty standard for people: when you are writing tests for module functions, then group them in `describe` blocks that will contain name (and arity) of the function in the name. Example: @@ -89,7 +94,8 @@ This allows me to see what functionality I am testing. Of course that doesn't apply to the Phoenix controllers, as there we do not test functions, but tuples in form `{method, path}` which I then write as `METHOD -path`, for example `POST /users`. +path`, for example `POST /users`. But the idea still stands - `describe` block +provide immediate context about what is tested. ## Avoid module mocking @@ -100,10 +106,10 @@ different modules to run in parallel (not single tests, *modules*, but that is probably material for another post). Instead of mocks I prefer to utilise dependency injection. Some people may argue -that "Elixir is FP, not OOP, there is no need for DI" and they cannot be further -from truth. DI isn't related to OOP, it just have different form, called -function arguments. For example, if we want to have function that do something -with time, in particular - current time. Then instead of writing: +that "Elixir is FP, not OOP, there is no need for dependency injection". They +could not be further from truth. DI isn't related to OOP, it just have different +form - function arguments. For example, if we want to have function that do +something with time, in particular - current time, then instead of writing: ```elixir def my_function(a, b) do @@ -121,9 +127,20 @@ end ``` Which still provide me the ergonomics of `my_function/2` as above, but is way -easier to test, as I can pass the date to the function itself. This allows me to -run this test in parallel as it will not cause other tests to do weird stuff -because of altered `DateTime` behaviour. +easier to test, as I can pass the date to the function itself. Now I can run +this test in parallel as it will not cause other tests to do weird stuff because +of altered `DateTime` behaviour. + +This approach I use a lot when I am writing some functions that are doing +HTTP(S) requests to external services. I use optional[^opt-arg] keyword list +argument called with super creative name `opts`. With that, I can pass option +like `:host` which allows me to use tools like [`test_server`][] which is great +and it is, in my humble opinion, way better approach than any mocking. + +[^opt-arg]: Arguments in Elixir never are optional, it is a clever trick from + language authors, but that is probably for another article. + +[`test_server`]: https://github.com/danschultzer/test_server ## Avoid `ex_machina` factories @@ -178,7 +195,7 @@ defmodule MyApp.Factory do %MyApp.Comment{ text: "It's great!", article: build(:article), - author: build(:user) # That line is added by me + author: build(:user) } end end @@ -223,7 +240,7 @@ And this is simplified example. In the past I needed to deal with situations where I was creating a lot of data to pass through custom attributes to make test sensible. -Instead I prefer to do stuff directly in code. Instead of relying on some +Instead, I prefer to do stuff directly in code. Instead of relying on some "magical" functions provided by some "magical" macros from external library I can use what I already have - functions in my application. @@ -263,13 +280,13 @@ end It may be a little bit more verbose, but it makes tests way more readable in my opinion. You have all details just in place and you know what to expect. And if you need some piece of data in all (or almost all) tests within -module/`describe` block, then you can always can use `setup/1` blocks. Or you -can create function per module that will generate data for you. As long as your -test module is self-contained and do not receive "magical" data out of thin air, -it is ok for me. But `ex_machina` is, in my opinion, terrible idea brought from -Rails world, that make little to no sense in Elixir. +module/`describe` block, then you can always can use `setup/1` blocks. +Alternatively, you can create function per module that will generate data for +you. As long as your test module is self-contained and do not receive "magical" +data out of thin air, it is okay for me. But `ex_machina` is, in my opinion, +terrible idea brought from Rails world, that make little to no sense in Elixir. -If you really need such factories, then just write your own functions that will +If you *really* need such factories, then just write your own functions that will use your contexts instead of relying on another library. For example: ```elixir @@ -288,4 +305,52 @@ end ``` This way you do not need to check if all tests use correct validations any -longer, as your system will do that for you. +longer, as your system will do that for you. No more surprises related to +dealing with "impossible data". + +## Property testing is awesome + +[Property based Testing][PBT] is absurdly wide topic, and this article isn't a +place to describe all possibilities of that approach. There are [books][pbt-erl] +written about this topic. However, it is something I find useful to keep in mind, +that this approach exists and is tremendously useful in many places (not all, do +not try to squeeze square peg into round hole). + +Quick example that I can show is: + +```elixir +property "valid names starts with alphanumeric and are composed of alphanumeric, underscore, and dash" do + check all( + prefix <- string(:alphanumeric, length: 1), + suffix <- string([?a..?z, ?A..?Z, ?-, ?_], max_length: 24) + ) do + changeset = + @subject.register_changeset(%@subject{}, %{name: prefix <> suffix}) + + assert nil == changeset.errors[:name] + end +end +``` + +It checks whether all usernames, that start with alphanumeric character and is +followed by sequence of up to 24 characters containing alphanumeric characters +with addition of dash and underscore, pass through validation. + +This test will try to generate *random* set of possible usernames, and then +check if all of them pass the test. If something fails, then system will try to +reduce found example to create minimal example that fails. It is not fully +deterministic test (the output may depend on randomly selected seed), but if +used correctly with other tests, it will greatly improve confidence in tests. + +## Parting words + +Tests should be readable, often even more readable than the code itself. Good +test suite can help you with confidence in your changes. In current world, full +of agentic coding, tests became even more important, as with good tests suite +you can give your agents more autonomy. + +Furthermore, I think that mocking must be destroyed. + +[PBT]: https://en.wikipedia.org/wiki/Property_testing +[pbt-erl]: https://propertesting.com +[claude-cc]: https://www.anthropic.com/engineering/building-c-compiler -- 2.51.2