Patch.Mock.Code (patch v0.10.1) View Source
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 funtion 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 funtion 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 funtion 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.
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
Specs
binary_error() :: :badfile | :nofile | :not_purged | :on_load_failure | :sticky_directory
Specs
chunk_error() :: :chunk_too_big | :file_error | :invalid_beam_file | :key_missing_or_invalid | :missing_backend | :missing_chunk | :not_a_beam_file | :unknown_chunk
Specs
compiler_option() :: term()
Specs
export_classification() :: :builtin | :generated | :normal
Specs
Specs
form() :: term()
Specs
load_error() :: :embedded | :badfile | :nofile | :on_load_failure
Specs
option() :: Patch.Mock.exposes_option()
Sum-type of all valid options
Link to this section Functions
Specs
abstract_forms(module :: module()) :: {:ok, [form()]} | {:error, :abstract_forms_unavailable} | {:error, chunk_error()} | {:error, load_error()}
Extracts the abstract_forms from a module
Specs
Extracts the attribtues from a module
Specs
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.
Specs
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.
Specs
compiler_options(module :: module()) :: {:ok, [compiler_option()]} | {:error, :compiler_options_unavailable} | {:error, chunk_error()} | {:error, load_error()}
Extracts the compiler options from a module.
Specs
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.
Specs
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
Specs
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.
Specs
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.
Specs
Purges a module from the code server
Specs
stick_module(module :: module()) :: :ok | {:error, load_error()}
Marks a module a sticky
Specs
unstick_module(module :: module()) :: {:ok, boolean()} | {:error, load_error()}
Unsticks a module
Returns {:ok, was_sticky?} on success, {:error, reason} otherwise