View Source Patch (patch v0.14.0)
Patch - Ergonomic Mocking for Elixir
Patch makes it easy to mock one or more functions in a module returning a value or executing custom logic. Patches and Spies allow tests to assert or refute that function calls have been made.
Using Patch is as easy as adding a single line to your test case.
use Patch
After this all the patch functions will be available, see the function documentation for details.
Link to this section Summary
Functions
Asserts that the given module and function has been called with any arity.
Asserts that the given module and function has been called with any arity.
Given a call will assert that a matching call was observed by the patched function.
Given a call will assert that a matching call was observed exactly the number of times provided by the patched function.
Given a call will assert that a matching call was observed exactly once by the patched function.
Enable or disable library level debugging.
Expose can be used to turn private functions into public functions for the purpose of testing them.
Fakes out a module with an alternative implementation.
Get all the observed calls to a module. These calls are expressed as a {name, argument}
tuple
and can either be provided in ascending (oldest first) or descending (newest first) order by
providing a sorting of :asc
or :desc
, respectively.
Starts a listener process.
Patches a function in a module
Suppress warnings for using exposed private functions in tests.
Suppress warnings for using exposed private functions in tests.
Gets the real module name for a fake.
Refutes that the given module and function has been called with any arity.
Refutes that the given module and function has been called with any arity.
Given a call will refute that a matching call was observed by the patched function.
Given a call will refute that a matching call was observed exactly the number of times provided by the patched function.
Given a call will refute that a matching call was observed exactly once by the patched function.
Convenience function for replacing part of the state of a running process.
Remove any mocks or spies from the given module
Remove any patches associated with a function in a module.
Spies on the provided module
Link to this section Functions
Asserts that the given module and function has been called with any arity.
patch(Example, :function, :patch)
assert_any_call Example.function # fails
Example.function(1, 2, 3)
assert_any_call Example.function # passes
Asserts that the given module and function has been called with any arity.
patch(Example, :function, :patch)
assert_any_call Example, :function # fails
Example.function(1, 2, 3)
assert_any_call Example, :function # passes
This function exists for advanced use cases where the module or function are not literals in the
test code. If they are literals then assert_any_call/1
should be preferred.
Given a call will assert that a matching call was observed by the patched function.
This macro fully supports patterns and will perform non-hygienic binding similar to ExUnit's
assert_receive/3
and assert_received/2
.
patch(Example, :function, :patch)
Example.function(1, 2, 3)
assert_called Example.function(1, 2, 3) # passes
assert_called Example.function(1, _, 3) # passes
assert_called Example.function(4, 5, 6) # fails
assert_called Example.function(4, _, 6) # fails
Given a call will assert that a matching call was observed exactly the number of times provided by the patched function.
This macro fully supports patterns and will perform non-hygienic binding similar to ExUnit's
assert_receive/3
and assert_received/2
. Any binds will bind to the latest matching call
values.
patch(Example, :function, :patch)
Example.function(1, 2, 3)
assert_called Example.function(1, 2, 3), 1 # passes
assert_called Example.function(1, _, 3), 1 # passes
Example.function(1, 2, 3)
assert_called Example.function(1, 2, 3), 2 # passes
assert_called Example.function(1, _, 3), 2 # passes
Given a call will assert that a matching call was observed exactly once by the patched function.
This macro fully supports patterns and will perform non-hygienic binding similar to ExUnit's
assert_receive/3
and assert_received/2
.
patch(Example, :function, :patch)
Example.function(1, 2, 3)
assert_called_once Example.function(1, 2, 3) # passes
assert_called_once Example.function(1, _, 3) # passes
Example.function(1, 2, 3)
assert_called_once Example.function(1, 2, 3) # fails
assert_called_once Example.function(1, _, 3) # fails
@spec debug(value :: boolean()) :: :ok
Enable or disable library level debugging.
There is a suite level configuration that can be set by using
config :patch,
debug: true # or false
Calling this helper will enable or disable debugging for a given test.
Library level debugging can be useful when patch behavior isn't meeting expectations. Additional logging will occur at the debug log level using Logger to provide insight into how Patch is working.
@spec expose(module :: module(), exposes :: Patch.Mock.exposes()) :: :ok | {:error, term()}
Expose can be used to turn private functions into public functions for the purpose of testing them.
To expose every private function as a public function, pass the sentinel value :all
.
expose(Example, :all)
Otherwise pass a Keyword.t(arity)
of the functions to expose.
For example, if one wanted to expose private_function/1
and private_function/2
.
expose(Example, [private_function: 1, private_function: 2])
After exposing a function, attempting to call the exposed function will cause the Elixir
Compiler to flag calls to exposed functions as a warning. There are companion macros
private/1
and private/2
that test authors can wrap their calls with to prevent warnings.
Fakes out a module with an alternative implementation.
The real module can still be accessed with real/1
.
For example, if your project has the module Example.Datastore
and there's a fake available in the testing
environment named Example.Test.InMemoryDatastore
the following table describes which calls are executed by which
code before and after faking with the following call.
fake(Example.Datastore, Example.Test.InMemoryDatastore)
Calling Code | Responding Module before fake/2 | Responding Module after fake/2 |
---|---|---|
Example.Datastore.get/1 | Example.Datastore.get/1 | Example.Test.InMemoryDatastore.get/1 |
Example.Test.InMemoryDatastore.get/1 | Example.Test.InMemoryDatastore.get/1 | Example.Test.InMemoryDatastore.get/1 |
real(Example.Datastore).get/1 | (UndefinedFunctionError) | Example.Datastore.get/1 |
The fake module can use the renamed module to access the original implementation.
@spec history(module :: module(), sorting :: :asc | :desc) :: [ Patch.Mock.History.entry() ]
Get all the observed calls to a module. These calls are expressed as a {name, argument}
tuple
and can either be provided in ascending (oldest first) or descending (newest first) order by
providing a sorting of :asc
or :desc
, respectively.
Example.example(1, 2, 3)
Example.function(:a)
assert history(Example) == [{:example, [1, 2, 3]}, {:function, [:a]}]
assert history(Example, :desc) == [{:function, [:a]}, {:example, [1, 2, 3]}]
For asserting or refuting that a call happened the assert_called/1
, assert_any_call/2
,
refute_called/1
, and refute_any_call/2
functions provide a more convenient API.
@spec inject( tag :: Patch.Listener.tag(), target :: Patch.Listener.target(), keys :: [term(), ...], options :: [Patch.Listener.option()] ) :: {:ok, pid()} | {:error, :not_found} | {:error, :invalid_keys}
@spec listen( tag :: Patch.Listener.tag(), target :: Patch.Listener.target(), options :: [Patch.Listener.option()] ) :: {:ok, pid()} | {:error, :not_found}
Starts a listener process.
Each listener should provide a unique tag
that will be used when forwarding messages to the
test process.
named-processes
Named Processes
When used on a named process, this is sufficient to begin intercepting all messages to the named process.
listen(:listener, Example)
unnamed-processes
Unnamed Processes
When used on an unnamed process, the process that is spawned will forward any messages to the caller and target process but any processes holding a reference to the old pid will need to be updated.
replace/3
can be used to replace part of a running process with the listener
{:ok, listener} = listen(:listener, original)
replace(target, [:original], listener)
inject/3
provides a nice convenience when you want to wrap a listener around the pid in the
state.
inject(:listener, target, [:original])
This code will look for the key :original
in the target
process's state, and wrap it with
a listener tagged with :listener
substituting-for-a-process
Substituting for a Process
Listeners can also act as a complete substitute for a process. This is useful in scenarios where one Process starts other Processes but starting those Processes is outside of the bounds of the test. In those cases you can start a "targetless listener."
replace/3
can be used to replace part of a running process with the substitute listener.
{:ok, listener} = listen(:listener)
replace(target, [:original], listener)
inject/3
provides a nice conveninece when the state already has a nil that you want to replace
with a listener
inject(:listener, target, [:original])
This code will look for the key :original
in the target
process's state, finding it nil
it
will create a "targetless listener" tagged with :listener
and put it in the state.
@spec patch(module :: module(), function :: atom(), value :: Patch.Mock.Value.t()) :: Patch.Mock.Value.t()
@spec patch(module :: module(), function :: atom(), callable) :: callable when callable: function()
@spec patch(module :: module(), function :: atom(), return_value) :: return_value when return_value: term()
Patches a function in a module
When called with a function the function will be called instead of the original function and its results returned.
patch(Example, :function, fn arg -> {:mock, arg} end)
assert Example.function(:test) == {:mock, :test}
To handle multiple arities create a callable/2
with the :list
option and the arguments will
be wrapped to the function in a list.
patch(Example, :function, callable(fn
[] ->
:zero
[a] ->
{:one, a}
[a, b] ->
{:two, a, b}
end, :list))
assert Example.function() == :zero
assert Example.function(1) == {:one, 1}
assert Example.function(1, 2) == {:two, 1, 2}
To provide a function as a literal value to be returned, use the scalar/1
function.
patch(Example, :function, scalar(fn arg -> {:mock, arg} end))
callable = Example.function()
assert callable.(:test) == {:mock, :test}
The function cycle/1
can be given a list which will be infinitely cycled when the function is
called.
patch(Example, :function, cycle([1, 2, 3]))
assert Example.function() == 1
assert Example.function() == 2
assert Example.function() == 3
assert Example.function() == 1
assert Example.function() == 2
assert Example.function() == 3
assert Example.function() == 1
The function raises/1
can be used to raise/1
a RuntimeError
when the function is called.
patch(Example, :function, raises("patched"))
assert_raise RuntimeError, "patched", fn ->
Example.function()
end
The function raises/2
can be used to raise/2
any exception with any attributes when the function
is called.
patch(Example, :function, raises(ArgumentError, message: "patched"))
assert_raise ArgumentError, "patched", fn ->
Example.function()
end
The function sequence/1
can be given a list which will be used until a single value is
remaining, the remaining value will be returned on all subsequent calls.
patch(Example, :function, sequence([1, 2, 3]))
assert Example.function() == 1
assert Example.function() == 2
assert Example.function() == 3
assert Example.function() == 3
assert Example.function() == 3
assert Example.function() == 3
assert Example.function() == 3
The function throws/1
can be given a value to throw/1
when the function is called.
patch(Example, :function, throws(:patched))
assert catch_throw(Example.function()) == :patched
Any other value will be returned as a literal scalar value when the function is called.
patch(Example, :function, :patched)
assert Example.function() == :patched
Suppress warnings for using exposed private functions in tests.
Patch allows you to make a private function public via the expose/2
function. Exposure
happens dynamically at test time. The Elixir Compiler will flag calls to exposed functions as a
warning.
One way around this is to change the normal function call into an apply/3
but this is
cumbersome and makes tests harder to read.
This macro just rewrites a normal looking call into an apply/3
so the compiler won't complain
about calling an exposed function.
expose(Example, :all)
patch(Example, :private_function, :patched)
assert Example.private_function() == :patched # Compiler will warn about call to undefined function
assert apply(Example, :private_function, []) == :patched # Compiler will not warn
assert private(Example.private_function()) == :patched # Same as previous line, but looks nicer.
Suppress warnings for using exposed private functions in tests.
Patch allows you to make a private function public via the expose/2
function. Exposure
happens dynamically at test time. The Elixir Compiler will flag calls to exposed functions as a
warning.
One way around this is to change the normal function call into an apply/3
but this is
cumbersome and makes tests harder to read.
This macro just rewrites a normal looking call into an apply/3
so the compiler won't complain
about calling an exposed function, with support for pipelines.
expose(Example, :all)
example_that_warns =
Example.new()
|> Example.private_function() # Compiler will warn about call to undefined function
example_that_does_not_warn
Example.new()
|> private(Example.private_function()) # Compiler will not warn and Example.new() is provided
# as the first argument to Example.private_function/1
Gets the real module name for a fake.
This is useful for Fakes that want to defer some part of the functionality back to the real module.
def Example do
def calculate(a) do
# ...snip some complex calculations...
result
end
end
def Example.Fake do
import Patch, only: [real: 1]
def calculate(a) do
real_result = real(Example).calculate(a)
{:fake, real_result}
end
end
Refutes that the given module and function has been called with any arity.
patch(Example, :function, :patch)
refute_any_call Example.function # passes
Example.function(1, 2, 3)
refute_any_call Example.function # fails
Refutes that the given module and function has been called with any arity.
patch(Example, :function, :patch)
refute_any_call Example, :function # passes
Example.function(1, 2, 3)
refute_any_call Example, :function # fails
This function exists for advanced use cases where the module or function are not literals in the
test code. If they are literals then refute_any_call/1
should be preferred.
Given a call will refute that a matching call was observed by the patched function.
This macro fully supports patterns.
patch(Example, :function, :patch)
Example.function(1, 2, 3)
refute_called Example.function(4, 5, 6) # passes
refute_called Example.function(4, _, 6) # passes
refute_called Example.function(1, 2, 3) # fails
refute_called Example.function(1, _, 3) # fails
Given a call will refute that a matching call was observed exactly the number of times provided by the patched function.
This macro fully supports patterns.
patch(Example, :function, :patch)
Example.function(1, 2, 3)
refute_called Example.function(1, 2, 3), 2 # passes
refute_called Example.function(1, _, 3), 2 # passes
Example.function(1, 2, 3)
refute_called Example.function(1, 2, 3), 1 # passes
refute_called Example.function(1, _, 3), 1 # passes
Given a call will refute that a matching call was observed exactly once by the patched function.
This macro fully supports patterns.
patch(Example, :function, :patch)
Example.function(1, 2, 3)
refute_called_once Example.function(1, 2, 3) # fails
refute_called_once Example.function(1, _, 3) # fails
Example.function(1, 2, 3)
refute_called_once Example.function(1, 2, 3) # passes
refute_called_once Example.function(1, _, 3) # passes
@spec replace(target :: GenServer.server(), keys :: [term(), ...], value :: term()) :: term()
Convenience function for replacing part of the state of a running process.
Uses the Access
module to traverse the state structure according to the given keys
.
Structs have special handling so that they can be updated without having to implement the
Access
behavior.
For example to replace the key :key
in the map found under the key :map
with the value
:replaced
replace(target, [:map, :key], :replaced)
Remove any mocks or spies from the given module
original = Example.example()
patch(Example, :example, :patched)
assert Example.example() == :patched
restore(Example)
assert Example.example() == original
Remove any patches associated with a function in a module.
original = Example.example()
patch(Example, :example, :example_patch)
patch(Example, :other, :other_patch)
assert Example.example() == :example_patch
assert Example.other() == :other_patch
restore(Example, :example)
assert Example.example() == original
assert Example.other() == :other_patch
@spec spy(module :: module()) :: :ok
Spies on the provided module
Once a module has been spied on the calls to that module can be asserted / refuted without changing the behavior of the module.
spy(Example)
Example.example(1, 2, 3)
assert_called Example.example(1, 2, 3) # passes