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
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 many 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
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.
Other Values
There are other types of values supported by Patch
, see Chapter 3: Mock Values
Callables with Multiple Arities
String.upcase
actually comes in 2 arities, String.upcase/1
and String.upcase/2
. In the above we only define a callable that handles arity 1 calls. A first attempt might be to provide multiple clauses in our anonymous function.
This code doesn't work
# The function can be patched to run custom code
patch(String, :upcase, fn
s ->
String.length(s)
s, _ ->
String.length(s)
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."
This is where the callable "dispatch mode" kicks in. 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
# The function can be patched to run custom code
patch(String, :upcase, callable(fn
[s | _] ->
String.length(s)
end, :list)
So now we have a function that gets a list of arguments. It only ever cares about the first argument so it pattern matches that value out. The second argument to callable/2
defines the "dispatch mode."
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
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
After a patch is applied, all subsequent calls to the module become "Observered Calls" and tests can assert that an expected call has occurred by using the assert_called
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
supports the :_
wildcard atom. In the above example the following assertion would also pass.
assert_called String.upcase(:_)
This can be useful when some of the arguments are complex or uninteresting for the unit test.
Tests can also refute that a call has occurred with the refute_called
macro. This macro works in much the same way as assert_called
and also supports the :_
wildcard atom.
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 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/2
and refute_any_call/2
functions.
These functions take two arguments the module and the function name as an atom.
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