View Source Patch (patch v0.13.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

Link to this macro

assert_any_call(call)

View Source (macro)
@spec assert_any_call(call :: Macro.t()) :: Macro.t()

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
Link to this function

assert_any_call(module, function)

View Source
@spec assert_any_call(module :: module(), function :: atom()) :: nil

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.

Link to this macro

assert_called(call)

View Source (macro)
@spec assert_called(Macro.t()) :: Macro.t()

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
Link to this macro

assert_called(call, count)

View Source (macro)
@spec assert_called(call :: Macro.t(), count :: Macro.t()) :: Macro.t()

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
Link to this macro

assert_called_once(call)

View Source (macro)
@spec assert_called_once(call :: Macro.t()) :: Macro.t()

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.

Link to this function

fake(real_module, fake_module)

View Source
@spec fake(real_module :: module(), fake_module :: module()) :: :ok

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 CodeResponding Module before fake/2Responding Module after fake/2
Example.Datastore.get/1Example.Datastore.get/1Example.Test.InMemoryDatastore.get/1
Example.Test.InMemoryDatastore.get/1Example.Test.InMemoryDatastore.get/1Example.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.

Link to this function

history(module, sorting \\ :asc)

View Source
@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.

Link to this function

inject(tag, target, keys, options \\ [])

View Source
@spec inject(
  tag :: Patch.Listener.tag(),
  target :: Patch.Listener.target(),
  keys :: [term(), ...],
  options :: [Patch.Listener.option()]
) :: {:ok, pid()} | {:error, :not_found} | {:error, :invalid_keys}
Link to this function

listen(tag, target \\ nil, options \\ [])

View Source
@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.

Link to this function

patch(module, function, value)

View Source
@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
@spec private(Macro.t()) :: Macro.t()

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.
Link to this macro

private(argument, call)

View Source (macro)
@spec private(Macro.t(), Macro.t()) :: Macro.t()

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
@spec real(module :: module()) :: module()

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
Link to this macro

refute_any_call(call)

View Source (macro)
@spec refute_any_call(call :: Macro.t()) :: Macro.t()

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
Link to this function

refute_any_call(module, function)

View Source
@spec refute_any_call(module :: module(), function :: atom()) :: nil

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.

Link to this macro

refute_called(call)

View Source (macro)
@spec refute_called(call :: Macro.t()) :: Macro.t()

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
Link to this macro

refute_called(call, count)

View Source (macro)
@spec refute_called(call :: Macro.t(), count :: Macro.t()) :: Macro.t()

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
Link to this macro

refute_called_once(call)

View Source (macro)
@spec refute_called_once(call :: Macro.t()) :: Macro.t()

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
Link to this function

replace(target, keys, value)

View Source
@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)
@spec restore(module :: module()) :: :ok | {:error, term()}

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
@spec restore(module :: module(), name :: atom()) :: :ok | {:error, term()}

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