View Source Repatch (Repatch v1.6.0)

Repatch is a library for efficient, ergonomic and concise mocking/patching in tests (or not tests). It provides an efficient and async-friendly replacement for Mox, ProtoMock, Patch, Mock and all other similar libraries.

Features

  • Patches any function or macro. Elixir or Erlang, private or public (except BIF/NIF).

  • Async friendly. With local, global, and allowances modes.

  • Boilerplate-free. But you still can leverage classic explicit DI with Repatch.

  • Call history.

  • Built-in async-friendly application env. See Repatch.Application.

  • Mock behaviour and protocol implementation generation. See Repatch.Mock

  • Supports expect-style mocking. See Repatch.Expectations

  • Testing framework agnostic. It even works in iex and remote shells.

Installation

def deps do
  [
    {:repatch, "~> 1.5"}
  ]
end

One-minute intro

for ExUnit users

  1. Add Repatch.setup() into your test_helper.exs file after the ExUnit.start()

  2. use Repatch.ExUnit in your test module

  3. Call Repatch.patch/3 to change implementation of any function in any module.

For example

defmodule ThatsATest do
  use ExUnit.Case, async: true
  use Repatch.ExUnit

  test "that's not a MapSet.new" do
    Repatch.patch(MapSet, :new, fn _list ->
      %{thats_not: :a_map_set}
    end)

    assert MapSet.new([1, 2, 3]) == %{thats_not: :a_map_set}

    assert Repatch.called?(MapSet, :new, 1)
  end
end

Summary

Types

Options passed in the allow/3 function.

Options passed in the called?/2 and called?/4. When multiple options are specified, they are combined in logical AND fashion.

Options passed in the fake/3 function.

Options passed in the history/1 function. When multiple options are specified, they are combined in logical AND fashion.

Mode of the patch, fake or application env isolation. These modes define the levels of isolation of the patches

Opaque value returned from notify/4 or notify/2. Can be used to receive the notification about the function call.

Options passed in the patch/4 function.

Options passed in all functions which trigger module recompilation

Options passed in the repatched?/4 function.

Options passed in the restore/4 function.

Options passed in the setup/1 function.

Options passed in the spy/2 function.

Debug metadata tag of the function. Declares if the function is patched and what mode was used for the patch.

Functions

Enables the allowed process to use the patch from owner process. Works only for patches in shared mode. See allow_option/0 for available options.

Lists all allowances of specified process (or self() by default). Works only when shared mode is enabled.

Checks if the function call is present in the history or not. First argument of this macro must be in a Module.function(arguments) format and arguments can be a pattern. It is also possible to specify a guard like in example.

Checks if the function call is present in the history or not. See called_check_option/0 for available options.

Cleans up current test process (or any other process) Repatch-state.

Replaces functions implementation of the real_module with functions of the fake_module.

Queries a history of all calls from current or passed process. It is also possible to filter by module, function name, arity, args and timestamps.

For debugging purposes only. Returns list of tags which indicates patch state of the specified function.

Patches a function to send the message to the calling process every time this function is successfully executed. Result of notify/2 can be used to be receivied on.

Patches a function to send the message to the calling process every time this function is successfully executed. Result of notify/4 can be used to be receivied on.

Returns current owner of the allowed process (or self() by default) if any. Works only when shared mode is enabled.

Substitutes implementation of the function with a new one. Starts tracking history on all calls in the module too. See patch_option/0 for available options.

Just a compiler-friendly wrapper to call private functions on the module. Works only on calls in Module.function(arg0, arg1, arg2) format.

Function version of the private/1 macro. Please try to use the macro version when possible, because macro has slightly better performance

Use this on a call which would result only to unpatched versions of functions to be called on the whole stack of calls. Works only on calls in Module.function(arg0, arg1, arg2) format.

Function version of the real/1 macro. Please prefer to use macro in tests, since it is slightly more efficient

Checks if function is patched in any (or some specific) mode.

Removes any patch on the specified function. See restore_option/0 for available options.

Clears all state of the Repatch including all patches, fakes and history, and reloads all modules back to their original bytecode, disabling history collection on them.

Setup function. Use it only once per test suite. See setup_option/0 for available options.

Tracks calls to the specified module.

Use this on a call which would result only on one unpatched version of the function to be called. Works only on calls in Module.function(arg0, arg1, arg2) format.

Function version of the Repatch.super/1 macro. Please try to use the macro version, since it is slightly more efficient.

Types

@type allow_option() :: {:force, boolean()}

Options passed in the allow/3 function.

  • force (boolean) — Whether to override existing allowance on the allowed process or not. Defaults to false.
@type called_check_option() ::
  {:by, GenServer.name() | pid() | :any}
  | {:after, monotonic_time_native :: integer()}
  | {:before, monotonic_time_native :: integer()}
  | {:at_least, :once | pos_integer()}
  | {:exactly, :once | pos_integer()}

Options passed in the called?/2 and called?/4. When multiple options are specified, they are combined in logical AND fashion.

  • by (GenServer.name/0 | :any | pid) — what process called the function. Defaults to self().

  • at_least (:once | integer) — at least how many times the function was called. Defaults to :once.

  • exactly (:once | integer) — exactly how many times the function was called.

  • before (:erlang.monotonic_time/0 timestamp) — upper boundary of when the function was called.
  • after (:erlang.monotonic_time/0 timestamp) — lower boundary of when the function was called.
@type fake_option() :: patch_option()

Options passed in the fake/3 function.

  • ignore_forbidden_module (boolean) — Whether to ignore the warning about forbidden module is being spied. Defaults to false.
  • force (boolean) — Whether to override existing patches and fakes. Defaults to false.
  • mode (:local | :shared | :global) — What mode to use for the patch. See mode/0 for more info. Defaults to :local.

@type history_option() ::
  {:by, GenServer.name() | pid() | :any}
  | {:after, monotonic_time_native :: integer()}
  | {:before, monotonic_time_native :: integer()}
  | {:module, module()}
  | {:function, atom()}
  | {:arity, arity()}
  | {:args, [term()]}

Options passed in the history/1 function. When multiple options are specified, they are combined in logical AND fashion.

@type mode() :: :local | :shared | :global

Mode of the patch, fake or application env isolation. These modes define the levels of isolation of the patches:

  • :local — Patches will work only in the process which set the patches
  • :shared — Patches will work only in the process which set the patches, spawned tasks or allowed processes. See Repatch.allow/2.
  • :global — Patches will work in all processes.

Please check out "Isolation modes" doc for more information on details.

@opaque notify_ref()

Opaque value returned from notify/4 or notify/2. Can be used to receive the notification about the function call.

@type patch_option() ::
  recompile_option() | {:mode, :local | :shared | :global} | {:force, boolean()}

Options passed in the patch/4 function.

  • ignore_forbidden_module (boolean) — Whether to ignore the warning about forbidden module is being spied. Defaults to false.
  • force (boolean) — Whether to override existing patches. Defaults to false.
  • mode (:local | :shared | :global) — What mode to use for the patch. See mode/0 for more info. Defaults to :local.

@type recompile_option() ::
  {:ignore_forbidden_module, boolean()}
  | {:module_binary, binary()}
  | {:recompile_only, [{module(), atom(), arity()}]}
  | {:recompile_except, [{module(), atom(), arity()}]}

Options passed in all functions which trigger module recompilation

  • ignore_forbidden_module (boolean) — Whether to ignore the warning about forbidden module being recompiled.
  • recompile_only (list of {module, function, arity} tuples) — Only these functions will be recompiled in this module or modules.
  • recompile_except (list of {module, function, arity} tuples) — All functions except specified will be recompiled in this module or modules.
  • module_binary (binary) — The BEAM binary of the module to recompile
Link to this type

repatched_check_option()

View Source
@type repatched_check_option() :: {:mode, mode() | :any}

Options passed in the repatched?/4 function.

  • mode (:local | :shared | :global | :any) — What mode to check the patch in. See mode/0 for more info. Defaults to :any.

@type restore_option() :: {:mode, mode()}

Options passed in the restore/4 function.

  • mode (:local | :shared | :global) — What mode to remove the patch in. See mode/0 for more info. Defaults to :local.

@type setup_option() ::
  recompile_option()
  | {:enable_global, boolean()}
  | {:enable_shared, boolean()}
  | {:enable_history, boolean()}
  | {:recompile, module() | [module()]}

Options passed in the setup/1 function.

  • enable_global (boolean) — Whether to allow global mocks in test suites. Defaults to false.
  • enable_shared (boolean) — Whether to allow shared mocks in test suites. Defaults to true.
  • enable_history (boolean) — Whether to enable calls history tracking. Defaults to true.
  • recompile (list of modules) — What modules should be recompiled before test starts. Modules are recompiled lazily by default. Defaults to [].
  • ignore_forbidden_module (boolean) — Whether to ignore the warning about forbidden module being recompiled. Works only when recompile is specified. Defaults to false.
  • cover (boolean) — Detected automatically by coverage tool, use with caution. Sets the line_counters native coverage mode.
@type spy_option() :: recompile_option() | {:by, pid()}

Options passed in the spy/2 function.

  • by (pid) — What process history to clean. Defaults to self().
  • ignore_forbidden_module (boolean) — Whether to ignore the warning about forbidden module is being spied. Defaults to false.
@type tag() :: :patched | mode()

Debug metadata tag of the function. Declares if the function is patched and what mode was used for the patch.

Functions

Link to this function

allow(owner, allowed, opts \\ [])

View Source
@spec allow(
  pid() | GenServer.name() | {atom(), node()},
  pid() | GenServer.name() | {atom(), node()},
  [allow_option()]
) :: :ok

Enables the allowed process to use the patch from owner process. Works only for patches in shared mode. See allow_option/0 for available options.

Example

iex> alias Repatch.Looper
iex> require Repatch
iex> pid = Looper.start_link()
iex> Range.new(1, 3)
1..3
iex> Repatch.patch(Range, :new, [mode: :shared], fn l, r -> Enum.to_list(Repatch.super(Range.new(l, r))) end)
iex> Range.new(1, 3)
[1, 2, 3]
iex> Looper.call(pid, Range, :new, [1, 3])
1..3
iex> Repatch.allow(self(), pid)
iex> Looper.call(pid, Range, :new, [1, 3])
[1, 2, 3]
Link to this function

allowances(pid \\ self())

View Source
@spec allowances(pid()) :: [pid()]

Lists all allowances of specified process (or self() by default). Works only when shared mode is enabled.

Please note that deep allowances are returned as the final allowed process.

Example

iex> alias Repatch.Looper
iex> require Repatch
iex> pid1 = Looper.start_link()
iex> pid2 = Looper.start_link()
iex> Repatch.allowances()
[]
iex> Repatch.allow(self(), pid1)
iex> Repatch.allowances()
[pid1]
iex> Repatch.allow(pid1, pid2)
iex> pid1 in Repatch.allowances() and pid2 in Repatch.allowances()
true
iex> Repatch.allowances(pid1)
[]
iex> Repatch.allowances(pid2)
[]
Link to this macro

called?(dotcall, opts \\ [])

View Source (since 1.6.0) (macro)

Checks if the function call is present in the history or not. First argument of this macro must be in a Module.function(arguments) format and arguments can be a pattern. It is also possible to specify a guard like in example.

See called_check_option/0 for available options.

Example

iex> Repatch.spy(Path)
iex> Repatch.called? Path.split("path/to")
false
iex> Path.split("path/to")
["path", "to"]
iex> Repatch.called? Path.split("path/to")
true
iex> Repatch.called? Path.split(string) when is_binary(string)
true
iex> Repatch.called? Path.split("path/to"), exactly: :once
true
iex> Path.split("path/to")
["path", "to"]
iex> Repatch.called? Path.split("path/to"), exactly: :once
false

Works only when history is enabled in setup. Please make sure that module is spied on or at least patched before querying history on it.

Link to this function

called?(module, function, arity_or_args, opts \\ [])

View Source
@spec called?(module(), atom(), arity() | [term()], [called_check_option()]) ::
  boolean()

Checks if the function call is present in the history or not. See called_check_option/0 for available options.

Example

iex> Repatch.spy(Path)
iex> Repatch.called?(Path, :join, 2)
false
iex> Path.join("left", "right")
"left/right"
iex> Repatch.called?(Path, :join, 2)
true
iex> Repatch.called?(Path, :join, 2, exactly: :once)
true
iex> Path.join("left", "right")
"left/right"
iex> Repatch.called?(Path, :join, 2, exactly: :once)
false

Works only when history is enabled in setup. Please make sure that module is spied on or at least patched before querying history on it.

@spec cleanup(pid()) :: :ok

Cleans up current test process (or any other process) Repatch-state.

Example

iex> Repatch.patch(DateTime, :utc_now, fn -> :ok end)
iex> DateTime.utc_now()
:ok
iex> Repatch.called?(DateTime, :utc_now, 0)
true
iex> Repatch.cleanup()
iex> Repatch.called?(DateTime, :utc_now, 0)
false
iex> %DateTime{} = DateTime.utc_now()

It is recommended to be called during the test exit. Check out Repatch.ExUnit module which set up this callback up.

Link to this function

fake(real_module, fake_module, opts \\ [])

View Source
@spec fake(module(), module(), [fake_option()]) :: :ok

Replaces functions implementation of the real_module with functions of the fake_module.

See fake_option/0 for available options.

Example

iex> ~U[2024-10-20 13:31:59.342240Z] != DateTime.utc_now()
true
iex> defmodule FakeDateTime do
...>   def utc_now do
...>     ~U[2024-10-20 13:31:59.342240Z]
...>   end
...> end
iex> Repatch.fake(DateTime, FakeDateTime)
iex> DateTime.utc_now()
iex> ~U[2024-10-20 13:31:59.342240Z] == DateTime.utc_now()
true
Link to this function

history(opts \\ [])

View Source (since 1.6.0)
@spec history([history_option()]) :: [
  {module :: module(), function :: atom(), args :: [term()],
   monotonic_timestamp :: integer()}
]

Queries a history of all calls from current or passed process. It is also possible to filter by module, function name, arity, args and timestamps.

See history_option/0 for available options.

Example

iex> Repatch.spy(Path)
iex> Repatch.spy(MapSet)
iex> Repatch.history()
[]
iex> Path.rootname("file.ex")
"file"
iex> MapSet.new()
MapSet.new([])
iex> Repatch.history(module: Path, function: :rootname)
[
  {Path, :rootname, ["file.ex"], -576460731414614326}
]
iex> Repatch.history()
[
  {Path, :rootname, ["file.ex"], -576460731414614326},
  {MapSet, :new, [], -576460731414614300},
  ...
]

Works only when history is enabled in setup. Please make sure that module is spied on or at least patched before querying history on it.

Link to this function

info(module, function, arity, pid \\ self())

View Source
@spec info(module(), atom(), arity(), pid()) :: [tag()]
@spec info(module(), atom(), arity(), :any) :: %{required(pid()) => [tag()]}

For debugging purposes only. Returns list of tags which indicates patch state of the specified function.

Example

iex> Repatch.patch(MapSet, :new, fn -> :not_a_mapset end)
iex> Repatch.info(MapSet, :new, 0)
[:patched, :local]
Link to this macro

notify(dotcall, opts \\ [])

View Source (since 1.6.0) (macro)

Patches a function to send the message to the calling process every time this function is successfully executed. Result of notify/2 can be used to be receivied on.

Example

iex> notification = Repatch.notify DateTime.utc_now()
iex> receive do ^notification -> :got after 0 -> :none end
:none
iex> DateTime.utc_now()
iex> receive do ^notification -> :got after 0 -> :none end
:got
Link to this function

notify(module, function, args_or_arity, opts \\ [])

View Source (since 1.6.0)
@spec notify(module(), atom(), arity() | [term()], [patch_option()]) :: notify_ref()

Patches a function to send the message to the calling process every time this function is successfully executed. Result of notify/4 can be used to be receivied on.

Example

iex> notification = Repatch.notify(DateTime, :utc_now, 0)
iex> receive do ^notification -> :got after 0 -> :none end
:none
iex> DateTime.utc_now()
iex> receive do ^notification -> :got after 0 -> :none end
:got

If you want to stop receiving notifications, you can call restore/4.

It is recommended to not use this function and instead use :trace module

@spec owner(pid()) :: pid() | nil

Returns current owner of the allowed process (or self() by default) if any. Works only when shared mode is enabled.

Please note that deep allowances are returned as the final owner of the process.

Example

iex> alias Repatch.Looper
iex> require Repatch
iex> pid1 = Looper.start_link()
iex> pid2 = Looper.start_link()
iex> Repatch.owner(pid1)
nil
iex> Repatch.allow(self(), pid1)
iex> Repatch.owner(pid1)
self()
iex> Repatch.allow(pid1, pid2)
iex> Repatch.owner(pid2)
self()
Link to this function

patch(module, function, opts \\ [], func)

View Source
@spec patch(module(), atom(), [patch_option()], function()) :: :ok

Substitutes implementation of the function with a new one. Starts tracking history on all calls in the module too. See patch_option/0 for available options.

Example

iex> ~U[2024-10-20 13:31:59.342240Z] != DateTime.utc_now()
true
iex> Repatch.patch(DateTime, :utc_now, fn -> ~U[2024-10-20 13:31:59.342240Z] end)
iex> DateTime.utc_now()
~U[2024-10-20 13:31:59.342240Z]

Be aware that it recompiles the module if it was not patched before, which may take some time.

And it is also recommended to not patch functions which can be changed in the future, since every patch is an implicit dependency on the internals of implementation.

Link to this macro

private(other)

View Source (macro)

Just a compiler-friendly wrapper to call private functions on the module. Works only on calls in Module.function(arg0, arg1, arg2) format.

Link to this function

private(module, function, args)

View Source
@spec private(module(), atom(), [term()]) :: any()

Function version of the private/1 macro. Please try to use the macro version when possible, because macro has slightly better performance

Use this on a call which would result only to unpatched versions of functions to be called on the whole stack of calls. Works only on calls in Module.function(arg0, arg1, arg2) format.

Example

iex> Repatch.patch(DateTime, :utc_now, fn _calendar -> :repatched end)
iex> DateTime.utc_now()
:repatched
iex> %DateTime{} = Repatch.real(DateTime.utc_now())
Link to this function

real(module, function, args)

View Source
@spec real(module(), atom(), [term()]) :: any()

Function version of the real/1 macro. Please prefer to use macro in tests, since it is slightly more efficient

Example

iex> Repatch.patch(DateTime, :utc_now, fn _calendar -> :repatched end)
iex> DateTime.utc_now()
:repatched
iex> %DateTime{} = Repatch.real(DateTime, :utc_now, [])
Link to this function

repatched?(module, function, arity, opts \\ [])

View Source
@spec repatched?(module(), atom(), arity(), [repatched_check_option()]) :: boolean()

Checks if function is patched in any (or some specific) mode.

Example

iex> Repatch.repatched?(MapSet, :new, 0)
false
iex> Repatch.patch(MapSet, :new, fn -> :not_a_mapset end)
iex> Repatch.repatched?(MapSet, :new, 0)
true
Link to this function

restore(module, function, arity, opts \\ [])

View Source
@spec restore(module(), atom(), arity(), [restore_option()]) :: :ok

Removes any patch on the specified function. See restore_option/0 for available options.

Example

iex> URI.encode_query(%{x: 123})
"x=123"
iex> Repatch.patch(URI, :encode_query, fn query -> inspect(query) end)
iex> URI.encode_query(%{x: 123})
"%{x: 123}"
iex> Repatch.restore(URI, :encode_query, 1)
iex> URI.encode_query(%{x: 123})
"x=123"
@spec restore_all() :: :ok

Clears all state of the Repatch including all patches, fakes and history, and reloads all modules back to their original bytecode, disabling history collection on them.

Example

iex> Repatch.patch(DateTime, :utc_now, fn -> :ok end)
iex> DateTime.utc_now()
:ok
iex> Repatch.restore_all()
iex> %DateTime{} = DateTime.utc_now()

It is not recommended to be called during testing and it is suggested to be used only when Repatch is used in iex session.

@spec setup([setup_option()]) :: :ok

Setup function. Use it only once per test suite. See setup_option/0 for available options.

Example

iex> Repatch.setup(enable_shared: false)

It is suggested to be put in the test_helper.exs after the ExUnit.start() line

@spec spy(module(), [spy_option()]) :: :ok

Tracks calls to the specified module.

Be aware that it recompiles the module if it was not patched or spied on before, which may take some time.

Example

iex> Repatch.spy(DateTime)
iex> DateTime.utc_now()
iex> Repatch.called?(DateTime, :utc_now, 0)
true
iex> Repatch.spy(DateTime)
iex> Repatch.called?(DateTime, :utc_now, 0)
false

If spy is called on the same module for more than one time, it will clear the history of calls.

Use this on a call which would result only on one unpatched version of the function to be called. Works only on calls in Module.function(arg0, arg1, arg2) format.

Example

iex> Repatch.patch(DateTime, :utc_now, fn _calendar -> :repatched end)
iex> DateTime.utc_now()
:repatched
iex> Repatch.super(DateTime.utc_now())
:repatched
iex> %DateTime{} = Repatch.super(DateTime.utc_now(Calendar.ISO))
Link to this function

super(module, function, args)

View Source
@spec super(module(), atom(), [term()]) :: any()

Function version of the Repatch.super/1 macro. Please try to use the macro version, since it is slightly more efficient.

Example

iex> Repatch.patch(DateTime, :utc_now, fn _calendar -> :repatched end)
iex> DateTime.utc_now()
:repatched
iex> Repatch.super(DateTime, :utc_now, [])
:repatched
iex> %DateTime{} = Repatch.super(DateTime, :utc_now, [Calendar.ISO])