View Source Chapter 4: Spies and Fakes

In Chapter 2: Patching and Chapter 3: Mock Values we saw how we can patch functions to return particular mock values.

There are two common cases for patching that have special helpers.

spies

Spies

If a test wishes to assert / refute calls that happen to a module without actually changing the behavior of the module it can simply spy/1 the module. Spies behave identically to the original module but all calls are recorded so assert_called/1, refute_called/1, assert_any_called/2, and refute_any_called/2 work as expected.

defmodule PatchExample do
  use ExUnit.Case
  use Patch

  def example(value) do
    String.upcase(value)
  end

  test "spies can see what calls happen without changing functionality" do
    spy(String)

    assert "HELLO" == example("hello")

    assert_called String.upcase("hello")
  end
end

fakes

Fakes

Sometimes we want to replace one module with another for testing, for example we might want to replace a module that connects to a real datastore with a fake that stores data in memory while providing the same API.

The fake/2,3 functions can be used to replace one module with another. The replacement module can be completely stand alone or can utilize the functionality of the replaced module, it will be made available through use of the real/1 function.

defmodule HighLatencyDatabase do
  @latency System.convert_time_unit(20, :second, :microsecond)

  def get(id) do
    {elapsed, response} = :timer.tc(fn -> Patch.real(Database).get(id) end)
    induce_latency(elapsed)
    response
  end

  defp induce_latency(elapsed) when elapsed < @latency do
    time_to_sleep = System.convert_time_unit(@latency - elapsed, :microsecond, :millisecond)
    Process.sleep(time_to_sleep)
  end

  defp induce_latency(_), do: :ok
end

This fake module uses the real module to actually get the record from the database and then makes sure that a minimum amount of latency, in this case 20 seconds, is introduced before returning the result.

To swap out our real Database with our fake HighLatencyDatabase in a test we can now do the following

defmodule PatchExample do
  use ExUnit.Case
  use Patch

  def example(value) do
    String.upcase(value)
  end

  test "API raises TimeoutError when database is experiencing high latency" do
    fake(Database, HighLatencyDatabase)

    assert_raises TimeoutError, fn ->
      API.get(:user, 1)
    end
  end
end