Testing function delegation in Elixir without stubbing

testing
Brian Cardarella

CEO & Founder

Brian Cardarella

In other lanugages mocking/stubbing are part of your regular toolbelt, in Elixir Jose has come out against them

Instead he suggests

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 CustomAdapters 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

Newsletter

Stay in the Know

Get the latest news and insights on Elixir, Phoenix, machine learning, product strategy, and more—delivered straight to your inbox.

Narwin holding a press release sheet while opening the DockYard brand kit box