Coming from the Ruby/Rails world I was searching for a way to mock the current time in my Elixir test suite, something like Timecop for Elixir. I didn’t find anything that suited my needs so I decided to give it a go by myself.
I had a module that contained following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
As you can see it depends on the current time. What if we want to test that an expired token is invalid? We have to encode the token, travel in time, and try to decode it.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
How can we travel in time? We have to manipulate the current time.
My main source of knowledge about mocking in Elixir was Mocks and explicit contracts by José Valim himself. The article suggests to avoid mocking (as a verb) and instead write mocks (as a noun). I was curious of this approach so I decided to give it a try.
I changed the implementation of current_timestamp/0
to:
1 2 3 |
|
Multipster.CurrentTime
is just an interface that delegates the function
call to a selected implementation:
1 2 3 4 5 6 7 8 9 |
|
The default implementation contains code that we already saw:
1 2 3 4 5 |
|
We configure it in config/confix.exs
:
1 2 |
|
The only environment where we want to use a different adapter is test
:
1 2 |
|
In order to implement our mock we have to write code that can keep state and allow
this state to be manipulated from tests. I decided to use Agent
for that,
because it seemed to be a recommended pattern.
The state that we want to keep in the agent is is_frozen
and frozen_value
.
Initially we start with an unfrozen time and we allow to change this state
from tests by providing freeze/0
, freeze/1
, and unfreeze/0
functions.
Of course we also have to implement the required interface – get_timestamp/0
.
Here we go:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
I was excited when writing this code as it was my first experience with message passing in Elixir!
We need one more thing to get the agent to work – we have to start it. We will
do it in test/test_helper.exs
:
1
|
|
And now we can travel in time:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
It works, yay!
Are we done yet? Not really, there are a few things to improve here.
They say that there’s no global state in Elixir, but here we have a similar problem – an agent holding state that isn’t magically reset between tests. All tests executed after the above one will use the frozen time. We don’t want that so we have to explicitly unfreeze the time.
Initially I went with:
1 2 3 4 5 6 7 8 9 |
|
The problem is that if {:error, _} = Token.decode(token)
doesn’t match we will
never execute Multipster.CurrentTime.Mock.unfreeze()
.
We need something like an after
hook from RSpec. ExUnit.Callbacks.on_exit/2
should do the job:
1 2 3 4 5 6 7 8 |
|
This also helps us maintain a logical order in the test – assertion at the end.
As we have only one agent in the whole application, concurrently running multiple tests that mess with the time is not safe. Two tests may attempt to change the current time at the very same moment. This can lead to random, hard-to-reproduce test failures. We have to specify:
1
|
|
I’d really appreciate input on how to improve my code so that tests can be safely run concurrently.
That said, this method allowed me to mock the time in my Elixir application, and I had a lot of fn while writing the code. I hope that you benefited from the blog post as well.