Patch.Mock.Code (patch v0.6.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
- 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
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
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
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
Classifies an exported mfa into one of the following classifications
Given a module and a list of exports filters the list of exports to those that have the given classification.
Mocks a module by generating a set of modules based on the target
module.
Link to this section Types
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
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()}
Specs
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, {:abstract_forms_invalid, [form()], term()}}
Specs
compiler_options(module :: module()) :: {:ok, [compiler_option()]} | {:error, :compiler_options_unavailable} | {:error, chunk_error()}
Specs
exports( abstract_forms :: [form()], module :: module(), exposes :: Patch.Mock.exposes() ) :: exports()
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
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 unchanged abstract_form is returned on success.