View Source Chapter 2: Patching
The most common operation a test author will perform with Patch
is, unsurprisingly, patching things.
When a module is patched, the patched function will return the mock value provided.
scalar-values
Scalar Values
The simplest kind of patch is one that just returns a static scalar value on every invocation.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "functions can be patched to return a specified value" do
# Assertion passes before patching
assert "HELLO" == String.upcase("hello")
# The function can be patched to return a static scalar value
patch(String, :upcase, :patched)
# Assertion passes after patching
assert :patched == String.upcase("hello")
end
end
No matter how many times we call String.upcase/1
from here on in and no matter what arguments we pass, we will always get back the value :patched
.
callable-values
Callable Values
Modules can also be patched to run custom logic instead of returning a static value
defmodule PatchExample do
use ExUnit.Case
use Patch
test "functions can be patched with a replacement function" do
# Assertion passes before patching
assert "HELLO" == String.upcase("hello")
# The function can be patched to run custom code
patch(String, :upcase, fn s -> String.length(s) end)
# Assertion passes after patching
assert 5 == String.upcase("hello")
end
end
Every time we call String.upcase/1
it will run our function and return the length of the input.
passthrough-vs-strict-evaluation
Passthrough vs Strict Evaluation
By default Patch
will evaluate the callable in :passthrough
mode. In passthrough mode if the callable raises either BadArityError
or FunctionClauseError
then the original function will be called. In :strict
mode these errors will be returned.
One of the core design principles underlying Patch
is that it tries to obey the intent of the test author. Consider the following example.
defmodule Example do
use GenServer
# Snip all the GenServer boilerplate
def handle_call({:a, argument}, _from, state) do
# Operation A Definition
{:reply, result, state}
end
def handle_call({:b, argument}, _from, state) do
# Operation B Definition
{:reply, result, state}
end
end
When a test author writes the following test code, how should Patch
interpret it?
patch(Example, :handle_call fn
{:a, _argument}, _from, state ->
{:reply, :ok, state}
end)
There are two possible interpretations, either the test author wants to patch the callback responsible for handling messages shaped like {:a, argument}
or the test author intends to replace all handle_call callback with one only capable of handling {:a, argument}
.
Patch
already has an opinion on which of these to prefer, we can see it in how Patch
handles patching out one function in a module. The assumption is that the test author will apply the minimum patch possible, so patching a single function in a module leaves all the other functions in the module as they were. Applying a similar "default passthrough" behavior to this situation it leads us to one conclusion, the test author probably just wants to replace the functionality handling {:a, argument}
and should leave the other callbacks alone.
There are always exceptions to the default rule, and Patch
wants to make it possible to express the other idea. The test author can inform Patch
of their intention by using the :strict
evaluation mode.
patch(Example, :handle_call, callable(fn
{:a, _argument}, _from, state ->
{:reply, :ok, state}
end, evaluate: :strict))
stacked-callables
Stacked Callables
Callables stack as they are defined in the test. Every time a function is patched with a callable, that callable is pushed to the top of the stack. When the patched function is called, the stack is walked from the top to bottom to find a callable that can handle it.
There are two problems that are nicely solved by Stacked Callables. Patching functions with multiple arities and making pattern matching in patching composable.
Stacking and Multiple Arities
The first problem that Stacked Callables solves is patching functions with multiple arities. Consider this example module.
defmodule Example do
def example(a) do
{:original, a}
end
def example(a, b, c) do
{:original, a, b, c}
end
end
This module defines two functions example/1
and example/3
. How can we patch both functions? Our first attempt might look something like this:
Note: this code is invalid and won't compile
patch(Example, :example, fn
a ->
{:patched, a}
a, b, c ->
{:patched, a, b, c}
end)
This code is illegal in Elixir, the compiler will throw a CompileError and explain that you "cannot mix clauses with different arities in anonymous functions."
The first solution for this was introduced in v0.6.0 and took the form of a "dispatch mode." By default Patch
will use the :apply
mode, which calls the function with the same arity as the patched function was called. There is an alternative "dispatch mode" called :list
which will pass all the arguments as a single argument, a list of the arguments.
This code will work, but is unwieldy
patch(Example, :example, callable(fn
[a] ->
{:patched, a}
[a, b, c] ->
{:patched, a, b, c}
end, dispatch: :list)
This solution made it possible to handle multiple arities, but it is pretty clunky. With stacked callables we can actually just define two separate callables, one for arity 1, and one for arity 3.
This code works and is easy to read and write
patch(Example, :example, fn a -> {:patched, a} end)
patch(Example, :example, fn a, b, c -> {:patched, a, b, c} end)
To understand how this works, let's look at how a call to example/1
and a call to example/3
work. The first thing we have to understand is what the Callable Stack looks like, so let's diagram it.
# Top of Stack (latest defined callable)
[
fn a, b, c -> {:patched, a, b, c} end,
fn a -> {:patched, a} end
]
# Bottom of Stack (earliest defined callable)
Patch
will run each function until one returns a valid value, the first function to respond with a return value will cause evaluation to complete.
When Example.example(1)
is evaluated it will try the first function. This function has a different arity so it will raise BadArityError
, this is one of the two errors that engages passthrough behavior. Since the first entry in the stack resulted in a logical passthrough Patch
will try the next entry. The next entry has the right arity and results in {:patched, 1}
being returned.
When Example.example(1, 2, 3)
is evaluated, it's a bit simpler. The first function is tried and it matches in arity so it immediately returns {:patched, 1, 2, 3}
and evaluation is completed.
Stacking and Matching
Another problem that Stacked Callables helps solve is composability when patching with pattern matching. Consider the following example module.
defmodule Example do
def handle(:a) do
{:original, :a}
end
def handle(:b) do
{:original, :b}
end
def handle(:c) do
{:original, :c}
end
end
If a test author wanted to provide patched behavior for :a
and :b
they can do so like this.
patch(Example, :handle, fn
:a ->
{:patched, :a}
:b ->
{:patched, :b}
end)
Which is a very convenient use of built-in Elixir feature, namely that anonymous functions can have multiple clauses. But what if we want to have a common behavior for patching out the handling of :a
and the handling of :b
. Perhaps in one test we want patch out :a
, in another :a
and :b
, and in a third just :b
. Is there any way that we can DRY up this code and make it composable?
Stacked Callables make this quite nice because it makes it possible to patch multiple clauses at different times.
defmodule ExampleTest do
use ExUnit
use Patch
def patch_a do
patch(Example, :handle, fn :a -> {:patched, a} end)
end
def patch_b do
patch(Example, :handle, fn :b -> {:patched, b} end)
end
test "that cares about :a" do
patch_a()
assert Example.handle(:a) == {:patched, :a}
end
test "that cares about :a and :b" do
patch_a()
patch_b()
assert Example.handle(:a) == {:patched, :a}
assert Example.handle(:b) == {:patched, :b}
end
test "that cares about :b" do
patch_b()
assert Example.handle(:b) == {:patched, :b}
end
end
Besides improving composability, it can also just make test code easier to read by breaking multiple logical patches into multiple calls.
Compare
patch(Example, :handle, fn
:a ->
{:patched, :a}
:b ->
{:patched, :b}
end)
with
patch(Example, :handle, fn :a -> {:patched, :a} end)
patch(Example, :handle, fn :b -> {:patched, :b} end)
The power of this mechanism becomes readily apparent when applied to something like GenServer
patch(Example, :handle_call, fn {:a, _args}, _from, state -> {:reply, :ok, state} end)
patch(Example, :handle_call, fn {:b, _args}, _from, state -> {:reply, :ok, state} end)
It is common for a GenServer to have many handle_call, handle_cast, and handle_info callbacks. Being able to define the patches by the pattern makes it easy to patch out a subset of the GenServer's behavior
functions-as-scalars
Functions as Scalars
If functions are always considered callable, how can we patch a function so that it returns a function literal? This can be accomplished by wrapping the function in a call to scalar/1
to turn it into a scalar.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "patch returns a function literal" do
patch(Example, :get_name_normalizer, scalar(&String.downcase/1))
normalizer = Example.get_name_normalizer()
assert normalizer.("Patch") == "patch"
end
end
other-values
Other Values
There are other types of values supported by Patch
, see Chapter 3: Mock Values
ergonomics
Ergonomics
patch/3
returns the value that the patch will return which can be useful for later on in the test. Examine this example code for an example
defmodule PatchExample do
use ExUnit.Case
use Patch
test "patch returns the patch" do
{:ok, expected} = patch(My.Module, :some_function, {:ok, 123})
# ... additional testing code ...
assert response.some_function_result == expected
end
end
This allows the test author to combine creating fixture data with patching.
asserting-refuting-calls
Asserting / Refuting Calls
After a patch is applied, all subsequent calls to the module become "Observed Calls" and tests can assert that an expected call has occurred by using the assert_called/1
macro.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "asserting calls on a patch" do
patch(String, :upcase, :patched)
assert :patched = String.upcase("hello") # Assertion passes after patching
assert_called String.upcase("hello") # Assertion passes after call
end
end
assert_called/1
supports full pattern matching and non-hygienic binds. This is similar to how ExUnit's assert_receive/3
and assert_received/2
work.
# Wildcards are supported
assert_called String.upcase(_)
# Pinned variables are supported
expected = "hello"
assert_called String.upcase(^expected)
# Unpinned variables are supported
assert_called String.upcase(argument)
assert argument == "hello"
Tests can also refute that a call has occurred with the refute_called/1
macro. This macro works in much the same way as assert_called/1
and has full pattern support.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "refuting calls on a patch" do
patch(String, :upcase, :patched)
assert "h" == String.at("hello", 0)
refute_called String.upcase("hello")
end
end
asserting-refuting-call-once
Asserting / Refuting Call Once
We can assert that a call has only happened once with the assert_called_once/1
macro. This assertion will only pass if the only one observed call matches.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "refuting a patch was called once" do
patch(String, :upcase, :patched)
assert_called_once String.upcase("hello") # Assertion fails before the function is called.
assert :patched == String.upcase("hello")
assert_called_once String.upcase("hello") # Assertion passes after called once.
assert :patched == String.upcase("hello")
assert_called_once String.upcase("hello") # Assertion fails after second call.
end
end
assert_called_once/1
supports patterns and binds just like assert_called/1
. In the above example the following assertion would behave identically.
# Wildcards are supported
assert_called_once String.upcase(_)
# Pinned variables are supported
expected = "hello"
assert_called_once String.upcase(^expected)
# Unpinned variables are supported
assert_called_once String.upcase(argument)
assert argument == "hello"
Tests can also refute that a call has occurred once with the refute_called_once/1
macro. This macro works in much the same way as assert_called_once/1
and has full pattern support.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "refuting calls on a patch" do
patch(String, :upcase, :patched)
refute_called_once String.upcase("hello") # Assertion passes before the function is called.
assert :patched == String.upcase("hello")
refute_called_once String.upcase("hello") # Assertion fails after called once.
assert :patched == String.upcase("hello")
refute_called_once String.upcase("hello") # Assertion passes after second call.
end
end
asserting-refuting-call-counts
Asserting / Refuting Call Counts
We can assert that a call has happened some given number of times exactly with the assert_called/2
macro. The second argument is the number of observed call matches there must be to pass.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "asserting 3 calls on a patch" do
patch(String, :upcase, :patched)
assert :patched == String.upcase("hello")
assert_called String.upcase("hello"), 3 # Assertion fails after first call.
assert :patched == String.upcase("hello")
assert_called String.upcase("hello"), 3 # Assertion fails after second call.
assert :patched == String.upcase("hello")
assert_called String.upcase("hello"), 3 # Assertion passes after third call.
end
end
assert_called/2
supports patterns and binds just like assert_called/1
. Since multiple calls might match any binds bind to the latest matching call.
In the above example the following assertion would behave identically.
# Wildcards are supported
assert_called String.upcase(_), 3
# Pinned variables are supported
expected = "hello"
assert_called String.upcase(^expected), 3
# Unpinned variables are supported
assert_called String.upcase(argument), 3
assert argument == "hello"
Tests can also refute that a call has happened an exact number of times with the refute_called/2
macro. This macro works in much the same way as assert_called/2
and also has full pattern support.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "refuting 3 calls on a patch" do
patch(String, :upcase, :patched)
assert :patched == String.upcase("hello")
refute_called String.upcase("hello"), 3 # Assertion passes after first call.
assert :patched == String.upcase("hello")
refute_called String.upcase("hello"), 3 # Assertion passes after second call.
assert :patched == String.upcase("hello")
refute_called String.upcase("hello"), 3 # Assertion fails after third call.
end
end
asserting-refuting-multiple-arities
Asserting / Refuting Multiple Arities
If a function has multiple arities that may be called based on different conditions the test author may wish to assert or refute that a function has been called at all without regards to the number of arguments passed.
This can be accomplished with the assert_any_call/1
and refute_any_call/1
functions.
defmodule PatchExample do
use ExUnit.Case
use Patch
test "asserting any call on a patch" do
patch(String, :pad_leading, fn s -> s end)
# This formatting call might provide custom padding characters based on
# time of day. (This is an obviously constructed example).
TimeOfDaySensitiveFormatter.format("Hello World")
assert_any_call String.pad_leading
end
end
Similarly we can refute any call
defmodule PatchExample do
use ExUnit.Case
use Patch
test "refuting any call on a patch" do
patch(String, :pad_leading, fn s -> s end)
assert {:error, :not_a_string} = TimeOfDaySensitiveFormatter.format(123)
refute_any_call String.pad_leading
end
end
Advanced Use Cases
The assert_any_call/2
and refute_any_call/2
functions take two arguments the module and the function name as an
atom. This allows some more advanced use cases where the module or function isn't known at test authoring time.
defmodule PatchExample
use ExUnit.Case
use Patch
test "asserting any call on normalizer" do
spy(Formatter)
normalizer = Formatter.get_normalizer()
assert_any_call Fromatter, normalizer # Assertion fails before call
Formatter.normalize("hello", with: normalizer)
assert_any_call Fromatter, normalizer # Assertion passes after call
end
end
Similarly we can refute any call
defmodule PatchExample
use ExUnit.Case
use Patch
test "refuting any call on normalizer" do
spy(Formatter)
normalizer = Formatter.get_normalizer()
refute_any_call Formatter, normalizer # Assertion passes before call
Formatter.normalize("hello", with: normalizer)
refute_any_call Formatter, normalizer # Assertion fails after call
end
end