Hey! I recorded two video courses!
If you like this article, you will also like the courses! Check them out here!
Hey folks, today’s article is a quick one, but let me start with the obligatory dad joke:
What's red and bad for your teeth?
A brick
Alrighty, today’s post is about a quick and easy approach to mocking in Elixir tests.
Update: After publishing this blog post, my good friend and favourite nit-picker, Tobi Pfeiffer, pointed out on X that the “Mock” described here is actually a Stub. Unsurprisingly, given that from the five smartest people in a room, Tobi is three of them, he was right. I added a section on this topic to the end of the post. Thank you, Tobi ❤️
🔗 The Problem
I’m a big believer in testing. I love writing tests and making them exhaustive. Writing tests is just as much of a skill as writing your application code, and the earlier you start practicing, the easier and faster it will become. As I develop my application, I frequently add small test cases that only contain assert false
to remind myself of edge cases that I need to handle. But whether you love writing tests or you only practice “guilt-based testing”, as Chris McCord does, there’s one problem that you will always encounter: how to mock external dependencies.
There are plenty of mocking libraries out there like mock, mox, or mimic, and they all work pretty well. AppSignal wrote a helpful guide for them here.
However, I’d like to propose a much simpler and dependency-free alternative to mocking your external dependencies. In fact, José Valim himself wrote about this approach already in 2015 here! The approach is simple: Just replace your dependency module with a mock during tests.
🔗 The Solution
Let’s say that you depend on the Stripe API in your application like this:
defmodule MyApp.Invoices do
def get_invoice_url(invoice_id) do
case Stripe.Invoice.retrieve(invoice_id) do
{:ok, %Stripe.Invoice{hosted_invoice_url: url}} -> {:ok, url}
{:error, %Stripe.ApiErrors{message: message}} -> {:error, message}
end
end
end
This code uses the wonderful StripityStripe library to interact with the Stripe API. But how can we mock this dependency in our tests? Easy! We change the Stripe module that our code uses based on the environment!
# config/config.exs
config :my_app, stripe: Stripe
# config/test.exs
config :my_app, stripe: MyApp.Support.StripeMock
and in our code, we fetch the dependency during compile time:
# lib/my_app/invoices.ex
defmodule MyApp.Invoices do
@stripe Application.compile_env(:my_app, :stripe)
def get_invoice_url(invoice_id) do
case @stripe.Invoice.retrieve(invoice_id) do
{:ok, %Stripe.Invoice{hosted_invoice_url: url}} -> {:ok, url}
{:error, %Stripe.ApiErrors{message: message}} -> {:error, message}
end
end
end
Note that we set the dependency during compile time and not runtime as José’s blog from 2015 describes. Since Elixir 1.17 (I believe), you get an error if you try to call a sub-module of a runtime dependency.
iex> stripe = Stripe
Stripe
iex> stripe.Invoice.retrieve("foobar")
error: invalid alias: "stripe.Invoice". If you wanted to define an alias,
an alias must expand to an atom at compile time but it did not, you may
use Module.concat/2 to build it at runtime. If instead you wanted to invoke
a function or access a field, wrap the function or field name in double quotes
└─ iex:3
** (CompileError) cannot compile code (errors have been logged)
We fix this error by defining the module during compile time using Application.compile_env/2
. Now, we can use @stripe.Invoice.retrieve/1
without error.
🔗 The Tests
Now that we have our mock in place, let’s see how we can test against it. First, let’s create some tests:
# test/my_app/invoices_test.exs
defmodule MyApp.InvoicesTest do
use MyApp.DataCase, async: true
alias MyApp.Invoices
describe "get_invoice_url/1" do
test "returns an invoice url" do
assert {:ok, url} = Invoices.get_invoice_url("in_12345")
assert url == "https://stripe.com/invoice/in_12345"
end
test "returns an error if the API key misses permissions" do
assert {:error, message} = Invoices.get_invoice_url("in_forbidden")
assert message == "The API key doesn’t have permissions to perform the request."
end
end
end
We wrote two tests: one for the happy path and one for the error path. We instruct our mock which response to return using the invoice ID. Let’s see how the mock looks like:
# test/support/stripe_mock.ex
defmodule MyApp.Support.StripeMock do
defmodule Invoice do
# Used in test/my_app/invoices_test.exs
def retrieve("in_forbidden") do
{:error, %Stripe.ApiErrors{message: "The API key doesn’t have permissions to perform the request."}}
end
def retrieve(invoice_id) do
{:ok, %Stripe.Invoice{hosted_invoice_url: "https://stripe.com/invoices/#{invoice_id}"}}
end
end
end
The mock is pretty basic, but it allows us to mirror the module structure of the StripityStripe library and to return the expected Stripe structs based on the arguments we receive from the tests. I like to add a comment indicating which test uses a special case, like the “in_forbidden” case, to make it easier to connect the mock clause with the test file in which it is used.
🔗 Mocks vs Stubs
After I published this blog post, my good friend, Tobi Pfeiffer, pointed out that the “Mock” described above is actually a “Stub”. He was right, so let’s understand why.
The difference between a Mock and a Stub is subtle and frequently leads to confusion between the two. In essence, a Stub is a dumb replacement of your dependency that only returns expected responses. A Stub does not validate its input arguments and has no test assertions. Your test may not fail if the Stub isn’t called at all.
A Mock, on the other hand, contains validations that can fail your tests if not fulfilled. It may validate the input arguments and return responses based on some internal logic. It might also validate your business rules against the input arguments. Your test can expect the Mock to be called and fail if that doesn’t happen. So, a Stub is a dumb replacement whereas a Mock is a replacement “with an attitude”, as Tobi says.
Luckily, we won’t have to change much to make our Stub a proper Mock. We’ll use a nifty trick that Tobi shared with Devon Estes, who kindly described it on his blog. The trick is to send a message from our Stub/Mock to our Test case with information about which function was called and with which arguments. Let’s have a look at the new Mock+Test setup:
# test/support/stripe_mock.ex
defmodule MyApp.Support.StripeMock do
defmodule Invoice do
def retrieve(invoice_id) do
send(self(), {:retrieve, invoice_id})
{:ok, %Stripe.Invoice{hosted_invoice_url: "https://stripe.com/invoices/#{invoice_id}"}}
end
end
end
# test/my_app/invoices_test.exs
defmodule MyApp.InvoicesTest do
use MyApp.DataCase, async: true
alias MyApp.Invoices
describe "get_invoice_url/1" do
test "returns an invoice url" do
assert {:ok, url} = Invoices.get_invoice_url("in_12345")
assert url == "https://stripe.com/invoice/in_12345"
assert_receive {:retrieve, "in_12345"}
end
end
end
This clever technique of sending a message to the test case enables us to assert that the mock was called with the correct arguments. With just two lines of code, we transformed our dumb Stub into a slightly smarter Mock, as we now assert not only that the mock is called but also that it’s called with the correct arguments.
However, the real question remains unanswered: Will “Well, actually”-Tobi approve of this new implementation or not? We shall see 😬
🔗 Conclusion
And that’s really it. I use this pattern in all my projects and tend to replace existing mocks with it when possible. It’s lightweight but expressive enough to support even large test suites. I like that I don’t have to repeat the same mock setup over and over again for every test case and file as I have to with some mocking libraries. I define a mock once and can use it everywhere. I also like that the setup in my application code is a one-liner at the top of the file.
I hope you enjoyed this article! If you want to support me, you can buy my book or video courses (one and two). Follow me on BlueSky or subscribe to my newsletter below if you want to get notified when I publish the next blog post. Until next time! Cheerio 👋