Computer Things
My Reflective Software business partner and I are currently looking for our next client. Learn more.

Make Elixir Tests Faster

See other articles in the “Elixir” topic.

Make Elixir Tests Faster

Everybody wants faster tests, and nobody wants to spend more time than necessary writing them. In this article, I will show a few ways to make Elixir tests faster.

Run Tests In Parallel

The most obvious way to make tests faster is to run them in parallel. ExUnit of course makes this easy with async: true:

defmodule MyTest do
  use ExUnit.Case, async: true

  # ...

But you have to remember to add async: true to every test, and remembering is hard, especially when there are multiple people working on the codebase. Luckily, Credo has a check called PassAsyncInTestCases . Just add the following to your .credo.exs in the Refactoring Opportunities section:

{Credo.Check.Refactor.PassAsyncInTestCases, []},

Credo will now fail if you don’t explicity add an async value (either true or false).

If you aren’t already using Credo, I’d recommend adding it. You can disable the checks you don’t care about or that would be too time-consuming to fix right now. I’d recommend running it in CI at the very least, but you might also want to run it before checking code in (via a commit hook or perhaps in your shipit script), or even directly in your editor.

Run Fewer Tests

mix test has a --stale option that only runs tests that reference files which have changed since the last time you ran tests with this option. If you get in the habit of running mix test --stale, you will end up saving time by running fewer tests. It’s probably a good idea to run the full suite before pushing code.

When running mix test --stale, you might be surprised at the number of tests that run. If your project has a big web of module dependencies, then changing one module might cause a lot of other modules to change, causing mix test --stale to run a lot of tests.

This is where the Boundary library comes in handy. It will help you enforce boundaries between different sections of your codebase, which will reduce the number of modules that get compiled when you make a change, which will cause fewer tests to run via mix test --stale, which will result in a faster test suite.

Installing Boundary is not hard. Fixing boundary violations can take time though. I’d recommend installing it as soon as possible, and then allowing existing boundary violations so that you can at least get alerted to new boundary violations. Over time, you can clean up the existing violations to further decrease compilation times and increase stale-test speed.

Shameless plug: my Reflective Software business partner and I can help with cleaning up your boundary violations so that you can continue building features.

Sleep Less

When testing some asynchronous code, your test sometimes has to wait for the async action to complete before it can successfully assert on the state. The easy way to do this is just drop in :timer.sleep(1_000) and hope that the async action has finished. Unfortunately, this has the dual downsides of making the test suite slower and more flaky.

A better way is to retry your assertion repeatedly until it passes. The Moar library (of which I am an author) has a Retry module with retry_for and rescue_for! functions:

# repeatedly retry up to 5 seconds for the element with ID "status" to be "finished"
Moar.Retry.rescue_for!(5000, fn ->
  assert view |> element("#status") |> render() == "finished"
end)

# repeatedly retry up to 5 seconds for the element with ID "status" to be "finished",
# and then make a different assertion
Moar.Retry.retry_for(5000, fn ->
  view |> element("#status") |> render() == "finished"
end)

assert view |> element("#total") |> render() == "8,675,309"

There is a downside to all that retrying and waiting: you are doing unnecessary work which may slow down the test suite (but probably not as much as sleeping for a long time does). Another approach is to send a message to your test when the thing you are waiting for has finished. Look for a future article about how to do that.


More “elixir” articles

Creating an RSS feed in Elixir and Phoenix
It's easy to hand-roll an RSS feed using Elixir and Phoenix.
Phoenix Project Layout
A nonstandard way to lay out your Phoenix project.
Querying HTML and XML in Elixir with HtmlQuery and XmlQuery
Find and extract information from HTML and XML using Elixir.
Web.Paths
A "Web.Paths" module in your Elixir Phoenix app can be a helpful indirection.
Why Boundary?
About the Boundary library, and why I highly recommend adding it to every Phoenix project from the very beginning.