I joined Podium for many reasons; among them was our dedication to testing. A good test suite gives you confidence to go fast and ship code often. The Elixir ecosystem has been a wonderful environment for producing tests that give me confidence in my code. The word Elixir has its roots in testing. Elixir is defined as:
A magical or medicinal potion.
That potion was probably brewed in a lab, with a test tube. (maybe it’s a stretch but go with me here)
Test Driven Development (TDD) is a practice I use to help me write better software. The basic process of TDD is; Write a failing test, make it pass, refactor the code. Elixir has a fantastic set of tools for doing TDD.
Components of a great TDD experience
- Fast test execution
- Describe behaviors
- Excellent Error messages
- Mockable dependancies
Let’s look at each of these in more detail.
Fast test execution
Having a fast test runner is crucial to a good TDD experience. When you make changes you need to know whether those changes failed or passed ASAP. Elixir uses Mix to execute tests, and ExUnit for writing tests. Lemme tell you, they are FAST.
Mix does a lot of really cool things to make your test suite fast by default, and ExUnit helps out too. Each test module you write is an elixir script, so it’s executed right away instead of compiling. Your source code is compiled, but when you run
mix test it only compiles the files that have changed since your last run. Take a look at the example below of a basic test module. (Shamelessly taken from the ExUnit Docs)
async: true flag in the example? That runs this test module concurrently with other tests in your suite. Now imagine your entire test suite runs concurrently, and only compiles source code when changed 🤯
In a typical TDD workflow, running tests against a real database slows your test suite down significantly. Not with Ecto. Take a look at how fast this test suite runs with about 3/4 of the tests doing actual Database work.
🎉 1.1 seconds 😍
This is possible because Ecto itself ships with a testing
sandbox mode that allows you to run concurrent tests against your database. Because
sandbox mode is so fast, it’s taken away my worry of testing against a real database.
Read more about it here.
Testing becomes much nicer when you can describe the intended behavior of your code with words. ExUnit gives us two macros for describing behavior:
test. These two macros are the building blocks of our tests. Here’s a contrived example of how you could use
A good pattern for labeling these
test blocks is to use
describe for the function name its arity, then use
test for each behavior that is wanted. In the example above, we wrap the
test blocks in
describe “add/2" saying that the add function, given two arguments, should pass these tests. Because the core building block of Elixir is just functions, all my tests follow this pattern! 🎉
The added benefit of writing our tests this way is we can target the exact behavior we want to change. It also makes for really nice error messages 😍
Notice the top line there? It prefixes our
test description with our
describe label. This is extremely helpful if you’re going to have a test suite of significant size.
Excellent Error Messages
Why do error messages help with TDD? The first step of TDD is important: make a failing test. Not only should the test fail, but it should be made to fail in exactly the way you want. I’ll be bold here:
Elixir has the best test runner error messages of any language I’ve seen
Let’s see why.
We already showed a basic example above. But let’s examine the structure again:
The clarity of error messages in elixir is what keeps me coming back. This simple example shows its awesome power. Let’s take a look at another example that is more complex.
Notice how we have our assert code pulled right from our test into our error message 🙌. Elixir also used a little pattern matching magic to show us exactly where our assertions are incorrect (notice Harry Potter in green and Rubeus Hagrid in Red).
When writing unit tests, it’s important to be able to test behaviors in isolation. It’s also nice to remove side effects to make tests run more consistently. Mocks help us do this. The elixir mocking ecosystem has a few options that handle mocking in their own way. I’m not going to talk about the way each of them do mocking, because they handle it differently. Here are some of the most popular options at them moment.
- Mox — https://github.com/plataformatec/mox
- Mock — https://github.com/jjh42/mock
- Mockery — https://github.com/appunite/mockery
A good mocking solution (at minimum) allows you to set return values on a mock, see what values a mock was called with, and see how many times a mock was called.
I’ve been able to find a really good TDD workflow in Elixir. The language maintainers take testing very seriously, and it shows in the tools that are available to the elixir community. Having an approachable testing framework has contributed greatly to Elixir’s success, and it’s one of the many reasons we‘ve made it our main language at Podium.