In other lanugages mocking/stubbing are part of your regular toolbelt, in Elixir Jose has come out against them
I will fight against mocks, stubs and YAML in Elixir with all my... friendliness and energy to promote proper education on those topics.
— José Valim (@josevalim) September 9, 2015
Instead he suggests
@bcardarella for tests, create a simple module (or an agent if you need flexibility) that will be used by your app during your tests
— José Valim (@josevalim) September 9, 2015
I’ve been trying to practice this until the other day when I was building a library that was adapter based. I wanted to unit test the parent module that would delegate to the adapter. The adapters can change and I don’t want the unit test of the parent module to be tied to any particular child. As a matter of example, we could have something like this:
defmodule Parent do
defmacro __using__([adapter: adapter]) do
quote do
def __adapter__, do: unquote(adapter)
def make_it_so(command) do
__adapter__.make_it_so(command)
end
end
end
end
In other languages I would stub out Parent.make_it_so/1
and assert that
this function was being called. For example, if you were using the
mock
Elixir library you would do:
defmodule CustomParent do
use Parent, adapter: FooBar
end
with_mock CustomParent, [make_it_so: fn(command) -> command end] do
CustomParent.make_it_so(:ok)
end
But as Jose has pointed out we don’t want to do this.
So how do we test that the adapter’s make_it_so/1
function is being
properly delegated to without stubbing? Well we can rely on Elixir’s
send/3
and assert_receive
.
Keep in mind that send
will allow you to put messages into a process’s
mailbox and assert_receive
will allow you to test against that.
Here is how you might test the delegation:
defmodule ParentTest do
use ExUnit.Case
defmodule CustomAdapter do
def make_it_so(_command) do
send self(), :ok
end
end
defmodule CustomParent do
use Parent, adapter: CustomAdapter
end
test "delegates to the adapter" do
CustomParent.make_it_so(%{foo: "bar"})
assert_receive :ok
end
end
And that’s it. You can handle more complex situations by adding your own
logic inside the CustomAdapter
s function, to send or not send
depending upon the value passed in but that should depend upon your
use-case.
So what happens when you are testing with a module that might spawn its
own process? In those cases I might have an opts
argument that I can
work with. Let’s assume that for whatever reason Parent.make_it_so
is working in a process on its own:
defmodule ParentTest do
use ExUnit.Case
defmodule CustomAdapter do
def make_it_so(_command, opts) do
send opts[:pid], :ok
end
end
defmodule CustomParent do
use Parent, adapter: CustomAdapter
end
test "delegates to the adapter" do
opts = [pid: self()]
CustomParent.make_it_so(%{foo: "bar"}, opts)
assert_receive :ok
end
end
This works because each test in ExUnit
runs in its own process. You
could even do this in a setup
block if you needed to capture the PID
for many tests. However, you cannot do this in setup_all
as that runs
in a different process than the individual tests.
Testing in Elixir has been fun as it has forced me to think about things differently than I’ve been used to over the past few years. If this topic is of interest to you check out my talk from ElixirDaze on Building and Testing Phoenix APIs