Writing Elixir stubs for better testing
In this post we’re going to see a handy technique to create and use stubs for your Elixir projects, leading to better tests, more maintainable code, and a lot of fun while using ETS, match specs, and macros.
15-20 minute read
Let’s jump right in: Untestable code
As an example, let’s say you have a module that uses some dependency to query DynamoDB (although note that this could be any other type of external service), and your code looks like this:
As you can see we don’t stand a chance to test this code without doing a real request to DynamoDB, because our code is tightly coupled to a specific implementation.
Experienced erlang devs would say “mock it”! Let’s dig a bit deeper into that option.
Stubs or mocks?
A stub is a piece of code that implements a contract, pre-programmed to return specific responses so you can test how your code behaves when handling those responses.
A mock, is similar, but also includes expectations (like “make sure this function is called N times with these args”, etc). You will usually verify all these expectations at the end of your test case.
Fine, use meck…
… or rather don’t. Meck is a mocking library for Erlang. It is sometimes used for stubbing rather than mocking.
It has a few downsides, as Brujo mentions in a previous post, including:
- Overhead to setup, tear down, and reset the mocked modules
- Difficulty running tests concurrently.
- Complex dependencies which increase as we mock more tests.
With these issues in mind, we should consider injecting our dependencies, and using stubs rather than mocking everything we need.
Making the code more testable
With only a small change, and by using module attributes we can really improve the initial situation and end up with this code:
And that’s how we’ve decoupled our code from a specific implementation. Now we can “inject” a stub and avoid using meck altogether.
This was actually discussed by José Valim in a 2015 post.
Injecting the stub
In this case, the way would be to define a config.exs
file like this:
And then a test.exs
file like this:
Then, when running mix test
the right module name will be compiled and used in our code (which is our stub).
Sample stub and test
Let’s create the stub for this dependency. We’re going to put it in test/lib/stub/dynamo.ex
.
The test is pretty straightforward:
Our code is completely decoupled from the dependency and we can go ahead and test every branch of it without issuing any requests to external actors.
Add our stub to the compile path
In our mix.exs
file, we should add a configuration for the compiler adding the path to where our stub modules are located (for example, test/lib
):
So are we done yet? Nah, not even close!
A good first step, still a long way to go
The proposed solution still has a few downsides:
1.- We have to add a function clause to our stub for every combination (more or less) of arguments to return the needed value.
2.- How do we match anything for some arguments but specific values for others?
Let’s see how ETS and match specifications can help us with these issues.
First improvement: Dynamic matching of arguments
Let’s change our stub to support “dynamic configuration”. For every stubbed function, we’re going to “record” the needed information in an ETS table and then for every call we’re going to try to find the matching function name and arguments from there.
Then the test code becomes:
And let’s not forget that we need to create the :dynamo_stub
ETS table, which can be done in test/test_helper.exs
:
Now there’s no need to create different function clauses per stubbed function. But wait, there’s more.
Second improvement: Matching for any argument
So there’s (at least) one thing remaining. How could we match for any argument? I.e: let’s say that we’d like our test to be like:
Since we’re already using ETS we can look at a useful erlang feature called “match specifications”.
What are match specifications?
To quote Match Specifications in erlang.org:
A “match specification” (match_spec) is an Erlang term describing a small “program” that tries to match something. It can be used to either control tracing with erlang:trace_pattern/3 or to search for objects in an ETS table with for example ets:select/2.
The main idea is to use Match Specifications to match the called function arguments and return the right value.
Match specifications are really complex, and we’re only going to use them to match the special :_
symbol as a wildcard meaning any argument.
For this we’re going to use ets:test_ms/2, it accepts a list of match specifications and tests it against a specific term.
Described in ets:select/2
This means that the match specification is always a list of one or more tuples (of arity 3).
The first element of the tuple is to be a pattern as described in match/2.
The second element of the tuple is to be a list of 0 or more guard tests.
The third element of the tuple is to be a list containing a description of the value to return. In almost all normal cases, the list contains exactly one term that fully describes the value to return for each object.
The return value is constructed using the “match variables” bound in MatchHead or using the special match variables ‘$_’ (the whole matching object).
Testing match specifications
Let’s give it a try in the Elixir console. If we want to stub a function with 3 arguments, we could use match specs like this:
Matching by exact argument values
Meaning that we’d like to match a 3-element tuple with exactly these arguments. No guards applied, and the result in case of a match will be the complete tuple.
Matching for any argument
Let’s say we’d like to only match arguments 1 and 3 and we don’t care about argument 2, we could do the following:
No arguments match
Cool. Now let’s try the case where we don’t match one or more args:
No results, so this works rather well for our use case, actually.
Updating our stub to use match specs
Now that we’ve learned how to use match specs for this, let’s change our stub module.
This is so much better. We don’t need to add function clauses with fixed arguments and return values in our stub module. Also, we can match on some but not all arguments as needed. But there’s still something else we can do.
Third improvement: Return the best match (less generic match)
What if a test setups our stub to return a value when called with the args :arg1, :arg2, :arg3
, while at the same another one sets it to return a different value for :arg1, :arg2, :_
?
We have a conflict there, we won’t know in advance which value is the right one to return to the caller since both configurations will match.
In cases like this, we’d like to always consistently return the result from the stub that best matches (i.e: a specific argument value should be preferred over a generic match like :_
).
An easy solution would be to sort all the matches from the match specs and use the one that has fewer :_
in it.
So one more time we go to our stub module:
Fourth improvement: Generic base stub module as a macro
As a “last” improvement (at least in terms of this post) we can wrap up our work by creating a generic stub module that can be reused in different mocks for our tests. We can do that by embedding our code inside a macro that can then be used in the different stubs:
Create an ETS table per stub
Since we’re going to have multiple module stubs we need an ETS table for each one of them, let’s set them up intest/test_helper.exs
:
Move the code into a generic stub module
The final code is pretty much the same, except that now we get the ETS table from the application environment:
Now our original stub will look like this:
And that’s pretty much it. Hopefully this will help you write more and better tests for your Elixir/Erlang code. Until next time :)
Conclusion
As a final word, it’s important to notice how much we gained by just investing 20 minutes more on refactoring our code to make it more testable, and to allow us to write better tests. By writing a few stubs and helpers, this allowed us to avoid mocking global modules, and in the end this will be really worth our while, our tests will run faster, will be smaller, and less complex.
Do you enjoy building high-quality large-scale systems? Roll with Us!