Patch (patch v0.12.0) View Source

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)

Specs

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

Specs

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)

Specs

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)

Specs

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)

Specs

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

Specs

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.

Specs

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

Specs

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

Specs

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

Specs

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, options \\ [])

View Source

Specs

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.

When used on a named process, this is sufficient to begin intercepting all messages to the named process.

listen(:listener, Example)

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.

inject/3 can be used to inject a listener into a running process.

{:ok, listener} = listen(:listener, original)
inject(target, :original, listener)
Link to this function

patch(module, function, value)

View Source

Specs

patch(module :: module(), function :: atom(), value :: Patch.Mock.Value.t()) ::
  Patch.Mock.Value.t()
patch(module :: module(), function :: atom(), callable) :: callable
when callable: function()
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

Specs

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)

Specs

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

Specs

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)

Specs

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

Specs

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)

Specs

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)

Specs

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)

Specs

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

Specs

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)

Specs

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

Specs

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

Specs

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