Elixir Test Doubles
A test double is a small stand-in module that acts like a real module during a test. The test uses the stand-in so the real module's slow or awkward work never runs.
A fake stands in for the real module
The behaviour promises one function, and the fake provides it. The sender takes the module as an argument, so the test passes the fake.
ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Messenger do
@callback deliver(String.t()) :: String.t()
end
defmodule FakeMessenger do
@behaviour Messenger
def deliver(name), do: "Hello, #{name}"
end
defmodule Sender do
def greet(mod, name), do: mod.deliver(name)
end
defmodule SenderTest do
use ExUnit.Case
test "greets through the fake" do
assert Sender.greet(FakeMessenger, "Ana") == "Hello, Ana"
end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")A fake returns a fixed answer
The fake clock always answers the same day. The reporter uses that answer, and the test checks the full sentence.
ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Clock do
@callback today() :: String.t()
end
defmodule FakeClock do
@behaviour Clock
def today(), do: "Monday"
end
defmodule Reporter do
def report(mod), do: "Today is #{mod.today()}"
end
defmodule ReporterTest do
use ExUnit.Case
test "reports the fixed day" do
assert Reporter.report(FakeClock) == "Today is Monday"
end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")Two fakes can share one behaviour
Both fakes follow the same behaviour but answer differently. Each test passes a different fake to the same function.
ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Notifier do
@callback send_message(String.t()) :: String.t()
end
defmodule QuietNotifier do
@behaviour Notifier
def send_message(_msg), do: "quiet"
end
defmodule LoudNotifier do
@behaviour Notifier
def send_message(_msg), do: "LOUD"
end
defmodule Announcer do
def announce(mod, msg), do: mod.send_message(msg)
end
defmodule AnnouncerTest do
use ExUnit.Case
test "announces quietly" do
assert Announcer.announce(QuietNotifier, "hi") == "quiet"
end
test "announces loudly" do
assert Announcer.announce(LoudNotifier, "hi") == "LOUD"
end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")Example
ExUnit.start(seed: 0, auto_run: false, formatters: [])
defmodule Store do
@callback fetch(String.t()) :: String.t()
end
defmodule FakeStore do
@behaviour Store
def fetch("Ana"), do: "Ana's card"
def fetch(_name), do: "unknown card"
end
defmodule Service do
def lookup(mod, name), do: mod.fetch(name)
end
defmodule ServiceTest do
use ExUnit.Case
test "finds a known name" do
assert Service.lookup(FakeStore, "Ana") == "Ana's card"
end
test "handles an unknown name" do
assert Service.lookup(FakeStore, "Bo") == "unknown card"
end
end
result = ExUnit.run()
IO.puts("passed: #{result.total - result.failures}/#{result.total}")Key points
- Test double: a small stand-in module that acts like a real module during a test.
- Behaviour: a module's promise of which functions it provides, written with callback lines.
- Fake: a test double that returns fixed answers instead of doing real work.
- Injection: passing the module to use as an argument, so the test can swap the fake in.
- Real work: the fake never touches the network, so the test stays fast and steady.
In this example, the Store behaviour promises a fetch function, and the fake store answers with a fixed card for a known name and an unknown card otherwise. Two tests pass the fake store to the service and check both answers.
Note
a fake must follow the same behaviour as the real module, or the test proves nothing about the real code.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Why is the module passed as an argument to the function under test?