View Source Patch.Mock.Code (patch v0.14.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
- Thetarget
module is replaced by afacade
module that intercepts all external calls and redirects them to thedelegate
module.original
- Thetarget
module is preserved as theoriginal
module, with the important transformation that all local calls are redirected to thedelegate
module.delegate
- This module is responsible for checking with theserver
to see if a call is mocked and should be intercepted. If so, the mock value is returned, otherwise theoriginal
function 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
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
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
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
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
delegate
module. - 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
end
external-function-calls
External Function Calls
First, let's examine how calls from outside the module are treated.
public-function-calls
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
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
Internal Calls
Next, let's examine how calls from outside the module are treated.
public-function-calls-1
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
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
Code Generation
For additional details on how Code Generation works, see the Patch.Mock.Code.Generate
module.
Link to this section 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
Link to this section 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
Link to this section 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