View Source Repatch (Repatch v1.3.0)
Repatch is a library for efficient, ergonomic and concise mocking/patching in tests (or not tests)
Features
Patch any function or macro (except NIF and BIF). Elixir or Erlang, private or public, it can be patched!
Designed to work with
async: true
. Has 3 isolation levels for testing multi-processes scenarios.Requires no boilerplate or explicit DI. Though, you are completely free to write in this style with Repatch!
Every patch is consistent and applies to direct or indirect calls and any process you choose.
Powerful call history tracking.
super
andreal
helpers for calling original functions.Works with other testing frameworks and even in environments like
iex
or remote shell.Get async-friendly application env! with just a single line in test. See
Repatch.Application
.
Installation
def deps do
[
{:repatch, "~> 1.0"}
]
end
One-minute intro
for ExUnit users
Add
Repatch.setup()
into yourtest_helper.exs
file after theExUnit.start()
use Repatch.ExUnit
in your test moduleCall
Repatch.patch
orRepatch.fake
to change implementation of any function and 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 ->
%{thats_not: :a_map_set}
end)
assert MapSet.new() == %{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?/4
function. When multiple options are specified, they are combined in logical AND fashion.
Options passed in the fake/3
function.
Mode of the patch, fake or application env isolation. These modes define the levels of isolation of the patches
Options passed in the patch/4
function.
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.
Works with exact match on arguments or just an arity.
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.
See called_check_option/0
for available options.
Cleans up current test process (or any other process) Repatch-state.
It is recommended to be called during the test exit.
Check out Repatch.ExUnit
module which set up this callback up.
Replaces functions implementation of the real_module
with functions
of the fake_module
.
For debugging purposes only. Returns list of tags which indicates patch state of the specified function.
Lists current owner of the allowed process (or self()
by default).
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 or fake 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
old modules back, disabling history collection on them. It is not recommended to be called
during testing and it is suggested to be used only when Repatch is used in iex session.
Setup function. Use it only once per test suite.
See setup_option/0
for available options.
Cleans the existing history of current process calls to this module and starts tracking new history of all 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 tofalse
.
@type called_check_option() :: {:by, pid() | :any} | {:at_least, :once | pos_integer()} | {:exactly, :once | pos_integer()} | {:after, monotonic_time_native :: integer()} | {:before, monotonic_time_native :: integer()}
Options passed in the called?/4
function. When multiple options are specified, they are combined in logical AND fashion.
by
(:any
| pid) — what process called the function. Defaults toself()
.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 tofalse
.force
(boolean) — Whether to override existing patches and fakes. Defaults tofalse
.mode
(:local
|:shared
|:global
) — What mode to use for the fake. Seemode/0
for more info. Defaults to:local
.
@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. SeeRepatch.allow/2
.:global
— Patches will work in all processes.
Please check out "Isolation modes" doc for more information on details.
@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 tofalse
.force
(boolean) — Whether to override existing patches and fakes. Defaults tofalse
.mode
(:local
|:shared
|:global
) — What mode to use for the patch. Seemode/0
for more info. Defaults to:local
.
@type recompile_option() :: {:ignore_forbidden_module, boolean()}
@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. Seemode/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. Seemode/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 tofalse
.enable_shared
(boolean) — Whether to allow shared mocks in test suites. Defaults totrue
.enable_history
(boolean) — Whether to enable calls history tracking. Defaults totrue
.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 whenrecompile
is specified. Defaults tofalse
.
@type spy_option() :: recompile_option() | {:by, pid()}
Options passed in the spy/2
function.
by
(pid) — What process history to clean. Defaults toself()
.ignore_forbidden_module
(boolean) — Whether to ignore the warning about forbidden module is being spied. Defaults tofalse
.
@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
@spec allow(pid(), pid(), [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]
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)
[]
Checks if the function call is present in the history or not.
Works with exact match on arguments or just an arity.
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.
See called_check_option/0
for available options.
Example
iex> Path.join("left", "right")
"left/right"
iex> Repatch.called?(Path, :join, 2)
false
iex> Repatch.patch(Path, :join, fn left, right -> left <> "|" <> right end)
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
@spec cleanup(pid()) :: :ok
Cleans up current test process (or any other process) Repatch-state.
It is recommended to be called during the test exit.
Check out Repatch.ExUnit
module which set up this callback up.
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()
@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
@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]
Lists current owner of the allowed process (or self()
by default).
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()
@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.
Be aware that it recompiles the module if it was not patched or spied on before, which may take some time.
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]
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.
Example
iex> Repatch.patch(DateTime, :utc_now, fn _calendar -> :repatched end)
iex> DateTime.utc_now()
:repatched
iex> %DateTime{} = Repatch.real(DateTime.utc_now())
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, [])
@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
@spec restore(module(), atom(), arity(), [restore_option()]) :: :ok
Removes any patch or fake 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
old modules back, disabling history collection on them. It is not recommended to be called
during testing and it is suggested to be used only when Repatch is used in iex session.
Example
iex> Repatch.patch(DateTime, :utc_now, fn -> :ok end)
iex> DateTime.utc_now()
:ok
iex> Repatch.restore_all()
iex> %DateTime{} = DateTime.utc_now()
@spec setup([setup_option()]) :: :ok
Setup function. Use it only once per test suite.
See setup_option/0
for available options.
It is suggested to be put in the test_helper.exs
after the ExUnit.start()
line
Example
iex> Repatch.setup(enable_shared: false)
@spec spy(module(), [spy_option()]) :: :ok
Cleans the existing history of current process calls to this module and starts tracking new history of all 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
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))
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])