Mocks in Elixir/Phoenix using Behaviors and Environment variables.
With Real World Examples Mocking SMS with Twilio.
Mocks simulate real-world behavior for the sake of convenient testing.
José Valim wrote an excellent piece on mocks and explicit contracts, and I recently had to opportunity to implement a Twilio Mock on my current project. I’ll share the experience of taking his advice and putting it to practice so that you can do the same.
The goal is to use a Twilio Sms Mock to verify and send. Rather than use the Twilio API directly, we don’t want to rely on external APIs in our non-production environments for a variety of reasons:
- Introduces flakiness and a reliance on network connectivity
- Slows down tests that need to wait for the response
- Avoid coupling code directly to the library in case we change libraries.
- Working directly with a library that can send real SMS text messages is risky.
We can set an environment variable in phoenix to point to the module we want to use depending on the environment.
Abstractly, there’s the parent module and the environment-specific modules it delegates to.
The rest of your application will use the parent module and never directly use the environment-specific modules. This avoids using the prod module directly, which could be disastrous 😰
In my specific use case, we want a single Sms module for the rest of the application to access. This module will delegate to either the production module or the mock module.
Access and Set Environment Variables.
You can set environment variables in phoenix by adding them to your environment’s config file.
To Set An Environment Variable.
Add the following to an environment config file.
- :app_name should be your applications name
- sms_service is the env variable name. It can be any valid name you want.
- App.Sms.MockSms is the SMS module you want to use for that environment. In dev.exs and test.exs, it should be MockSms, and in prod.exs it should be ProdSms
To Retrieve an Environment Variable.
You can access environment variables with
Create The Prod and Mock Modules.
Both modules should have the same interface. This means they must define the same common public methods.
Here’s an example prod module.
Here’s an example mock module.
The mock module skips sending messages and does soft validation on the phone number so that tests using invalid phone numbers will still return an error.
You don’t need to understand the implementation code to set up your own production and mock modules.
Your production and mock modules will differ from mine, depending on what you’re using this pattern for.
Delegate to the Prod and Mock Modules.
Now that you have production and mock modules defined, your parent module can delegate to them.
Here’s an example using the main Sms module.
Test the Prod Module.
To make sure your prod module continues to work, you can implement tests that specifically use the module.
However, you likely don’t want to run these tests as part of your normal suit, so you can tag them using @moduletag and exclude them from your normal tests.
To exclude the :sms tag, you can add this line in your test_helpers.exs.
You can then run these tests when needed using the include or only tags. You can learn about those in my article on useful test commands and tags.
Now that you have the parent module set up, you can use that in your normal tests.
José Valim went into deeper detail on all sorts of different patterns for using mocks. For further reading, I recommend his article on mocks and explicit contracts.
I hope my experience mocking our SMS module has been useful to you. If you have thoughts or questions, please share them with me either in the comments or on Twitter.
Just make sure you aren’t using your mock module in prod, otherwise, it might not send 😜