View Source Patch.Mock.Code (patch v0.16.0)
Patch mocks out modules by generating mock modules and recompiling them for a target module.
Patch's approach to mocking a module provides some powerful affordances.
- Private functions can be mocked.
- Internal function calls are effected by mocks regardless of the function's visibility without having to change the way code is written.
- Private functions can be optionally exposed in the facade to make it possible to test a private function directly without changing its visibility in code.
Mocking Strategy
There are 4 logical modules and 1 GenServer that are involved when mocking a module.
The 4 logical modules:
target- The module to be mocked.facade- Thetargetmodule is replaced by afacademodule that intercepts all external calls and redirects them to thedelegatemodule.original- Thetargetmodule is preserved as theoriginalmodule, with the important transformation that all local calls are redirected to thedelegatemodule.delegate- This module is responsible for checking with theserverto see if a call is mocked and should be intercepted. If so, the mock value is returned, otherwise theoriginalfunction is called.
Each target module has an associated GenServer, a Patch.Mock.Server that has keeps state
about the history of function calls and holds the mock data to be returned on interception. See
Patch.Mock.Server for more information.
Example Target Module
To better understand how Patch works, consider the following example module.
defmodule Example do
def public_function(argument_1, argument_2) do
{:public, private_function(argument_1, argument_2)}
end
defp private_function(argument_1, argument_2) do
{:private, argument_1, argument_2}
end
end
facade module
The facade module is automatically generated based off the exports of the target module.
It takes on the name of the provided module.
For each exported function, a function is generated in the facade module that calls the
delegate module.
defmodule Example do
def public_function(argument_1, argument_2) do
Patch.Mock.Delegate.For.Example.public_function(argument_1, argument_2)
end
end
delegate module
The delegate module is automatically generated based off all the functions of the target
module. It takes on a name based off the target module, see Patch.Mock.Naming.delegate/1.
For each function, a function is generated in the delegate module that calls
Patch.Mock.Server.delegate/3 delegating to the server named for the target module, see
Patch.Mock.Naming.server/1.
defmodule Patch.Mock.Delegate.For.Example do
def public_function(argument_1, argument_2) do
Patch.Mock.Server.delegate(
Patch.Mock.Server.For.Example,
:public_function,
[argument_1, argument_2]
)
end
def private_function(argument_1, argument_2) do
Patch.Mock.Server.delegate(
Patch.Mock.Server.For.Example,
:private_function,
[argument_1, argument_2]
)
end
end
original module
The original module takes on a name based off the provided module, see
Patch.Mock.Naming.original/1.
The code is transformed in the following ways.
- All local calls are converted into remote calls to the
delegatemodule. - All functions are exported
defmodule Patch.Mock.Original.For.Example do
def public_function(argument_1, argument_2) do
{:public, Patch.Mock.Delegate.For.Example.private_function(argument_1, argument_2)}
end
def private_function(argument_1, argument_2) do
{:private, argument_1, argument_2}
end
endExternal Function Calls
First, let's examine how calls from outside the module are treated.
Public Function Calls
Code calling Example.public_function/2 has the following call flow.
[Caller] -> facade -> delegate -> server -> mocked? -> yes (Intercepted)
[Mock Value] <----------------------------|----'
-> no -> original (Run Original Code)
[Original Value] <--------------------------------------'Calling a public function will either return the mocked value if it exists, or fall back to calling the original function.
Private Function Calls
Code calling Example.private_function/2 has the following call flow.
[Caller] --------------------------> facade
[UndefinedFunctionError] <-----'Calling a private function continues to be an error from the external caller's point of view.
The expose option does allow the facade to expose private functions, in those cases the call
flow just follows the public call flow.
Internal Calls
Next, let's examine how calls from outside the module are treated.
Public Function Calls
Code in the original module calling public functions have their code transformed to call the
delegate module.
original -> delegate -> server -> mocked? -> yes (Intercepted)
[Mock Value] <------------------|----'
-> no -> original (Run Original Code)
[Original Value] <----------------------------'Since the call is redirected to the delegate, calling a public function will either return the
mocked value if it exists, or fall back to calling the original function.
Private Function Call Flow
Code in the original module calling private functions have their code transformed to call the
delegate module
original -> delegate -> server -> mocked? -> yes (Intercepted)
[Mock Value] <------------------|----'
-> no -> original (Run Original Code)
[Original Value] <----------------------------'Since the call is redirected to the delegate, calling a private function will either return the
mocked value if it exists, or fall back to calling the original function.
Code Generation
For additional details on how Code Generation works, see the Patch.Mock.Code.Generate module.
Summary
Types
Sum-type of all valid options
Functions
Extracts the abstract_forms from a module
Extracts the attribtues from a module
Classifies an exported mfa into one of the following classifications
Compiles the provided abstract_form with the given compiler_options
Extracts the compiler options from a module.
Extracts the exports from the provided abstract_forms for the module.
Given a module and a list of exports filters the list of exports to those that have the given classification.
Freezes a module by generating a copy of it under a frozen name with all remote calls to the
target module re-routed to the frozen module.
Mocks a module by generating a set of modules based on the target module.
Purges a module from the code server
Marks a module a sticky
Unsticks a module
Types
@type binary_error() ::
:badfile | :nofile | :not_purged | :on_load_failure | :sticky_directory
@type chunk_error() ::
:chunk_too_big
| :file_error
| :invalid_beam_file
| :key_missing_or_invalid
| :missing_backend
| :missing_chunk
| :not_a_beam_file
| :unknown_chunk
@type compiler_option() :: term()
@type export_classification() :: :builtin | :generated | :normal
@type form() :: term()
@type load_error() :: :embedded | :badfile | :nofile | :on_load_failure
@type option() :: Patch.Mock.exposes_option()
Sum-type of all valid options
Functions
@spec abstract_forms(module :: module()) :: {:ok, [form()]} | {:error, :abstract_forms_unavailable} | {:error, chunk_error()} | {:error, load_error()}
Extracts the abstract_forms from a module
Extracts the attribtues from a module
@spec classify_export(module :: module(), function :: atom(), arity :: arity()) :: export_classification()
Classifies an exported mfa into one of the following classifications
- :builtin - Export is a BIF.
- :generated - Export is a generated function.
- :normal - Export is a user defined function.
@spec compile(abstract_forms :: [form()], compiler_options :: [compiler_option()]) :: :ok | {:error, binary_error()} | {:error, {:abstract_forms_invalid, [form()], term()}}
Compiles the provided abstract_form with the given compiler_options
In addition to compiling, the module will be loaded.
@spec compiler_options(module :: module()) :: {:ok, [compiler_option()]} | {:error, :compiler_options_unavailable} | {:error, chunk_error()} | {:error, load_error()}
Extracts the compiler options from a module.
@spec exports( abstract_forms :: [form()], module :: module(), exposes :: Patch.Mock.exposes() ) :: exports()
Extracts the exports from the provided abstract_forms for the module.
The exports returned can be controlled by the exposes argument.
@spec filter_exports( module :: module(), exports :: exports(), classification :: export_classification() ) :: exports()
Given a module and a list of exports filters the list of exports to those that have the given classification.
See classify_export/3 for information about export classification
Freezes a module by generating a copy of it under a frozen name with all remote calls to the
target module re-routed to the frozen module.
@spec module(module :: module(), options :: [option()]) :: {:ok, Patch.Mock.Code.Unit.t()} | {:error, term()}
Mocks a module by generating a set of modules based on the target module.
The target module's Unit is returned on success.
Purges a module from the code server
@spec stick_module(module :: module()) :: :ok | {:error, load_error()}
Marks a module a sticky
@spec unstick_module(module :: module()) :: {:ok, boolean()} | {:error, load_error()}
Unsticks a module
Returns {:ok, was_sticky?} on success, {:error, reason} otherwise