Skip to content
Roger Lipscombe edited this page May 25, 2023 · 6 revisions

New features in version 0.8 of Meck:

Honest Mocks (backwards compatibility breakage!)

Prior to release 0.8 Meck allowed you to create mocks of modules that did not exist on the code path, and create expectations for functions that either were not exported from their modules or did not exist their at all. We realized that the majority of such cases are probably user errors e.g.: a function in a mocked module was renamed, or deleted, or changed arity, but unit tests using it were not properly updated. Therefore such cases should be reported as errors.

Starting from release 0.8 mocking of a module missing from the code path fails with error {undefined_module, Module::atom()}, and expecting of a not-exported or nonexistent function fails with error {undefined_function, {Mod::atom(), Func::atom(), Arity::non_neg_integer()}}. It is still possible to revert to the original behavior when needed, but you have to do that explicitly creating a mock with non_strict option, e.g.: mock:new(bogus, [non_strict]).

New Syntax

Probably the most prominent feature of release 0.8 is the new expectation syntax. It was designed to allow definition of much more complex expectations in a more consistent, concise and intuitive way. Just as function in Erlang is defined as a list of clauses, where each clause is represented by a pair of an arguments pattern and a body that evaluates a return value, same way in Meck an expectation is defined as a list of expectation clauses, where every clause is represented by a pair of an arguments specification and a return specification.

meck:expect(foo, bar, [{[1, 1],     a},
                       {[1, '_'],   b},
                       {['_', '_'], c}]),
?assertMatch(a, foo:bar(1, 1)),
?assertMatch(b, foo:bar(1, 2)),
?assertMatch(c, foo:bar(2, 2)),

Whenever a mocked function (that is a function of a mocked module that has an expectation for it defined) is called, Meck first matches the arguments passed to the call against the argument specification of the first expectation clause. If it matches then the return specification of the first clause is used to produce a return value of the mocked call. Otherwise Meck checks the second expectation clause the same way. If the second clause does not match too, then it continues traversing the expectation clause list until the matching clause is found. If nothing is found then error function_clause is generated just as if it was for a regular function.

To define an expectation that consists of one clause only you can use meck:expect/4

meck:expect(foo, bar, [1, 1], a),
?assertMatch(a, foo:bar(1, 1)),
?assertError(function_clause, foo:bar(1, 2)),

Arguments Specification

An arguments specification can be defined either as a list of elements or as a non-negative integer.

If an arguments specification is defined as a list of elements, then every element is used to match with an argument passed to a function call at the respective position. If an element is given as atom '_' then it matches any value. In fact atom '_' can be used at any level in the element structure, hereby allowing partially specified patterns (e.g.: {1, [blah, {'_', "bar", 2} | '_'], 3}). Besides an arguments specification element can be a matcher that hides an arbitrary complex logic of decision making if a passed argument meets the matcher criterion or not. More on matchers read here.

meck:expect(foo, bar, [{['_', [{1, '_'} | '_']], a},
                       {['_', '_'],              b}]),
?assertMatch(a, foo:bar(2, [{1, 2}, {2, 2}])),
?assertMatch(b, foo:bar(2, [{2, 2}, {2, 2}])),

An arguments specification defined as a non-negative integer is equivalent to an arguments specification that is defined as a list of length equal to the integer and consisting solely of wildcards.

meck:expect(foo, bar, [{[1, 1], a},
                       {2,      b}]), % Equivalent to {['_', '_'], b}
?assertMatch(a, foo:bar(1, 1)),
?assertMatch(b, foo:bar(1, 2)),

In addition to meck:expect/3 arguments specifications are used throughout Meck everywhere where there is a need define to specify function call argument pattern. Namely in history digging functions meck:num_call and meck:called; and in just introduced meck:capture and meck:wait. That makes Meck even more consistent and intuitive.

Return Specification

Return Specification defines what a result an expectation clause produces. All possible Return Specifications are listed below along with functions that should be used to create them:

  • meck:val(Value::term()) The simplest form of Return Specification. Always evaluates to Value

    meck:expect(foo, bar, ['_'], meck:val(a)),
    ?assertMatch(a, foo:bar(1)),
    ?assertMatch(a, foo:bar(2)),

    NOTE that this kind of Return Specification has a less verbose form which is just Value, BUT with one exception, when Value is a function, then it is treated as meck:exec(Value). See below for details:

    meck:expect(foo, bar, ['_'], a),
    ?assertMatch(a, foo:bar(1)),
  • meck:passthrough() Passes arguments to the original function and returns the result of its evaluation,

  • meck:raise(Class::error|throw|exit, Reason::term()) When evaluated it results in an exception of the specified class with the specified reason.

    meck:expect(foo, bar, ['_'], meck:raise(error, blah)),
    ?asserError(blah, foo:bar(1)),
  • meck:exec(Body::fun()) Forwards return value evaluation to Body. Body MUST be of the same arity as the function for which an expectation is defined, otherwise a run-time error occurs. Arguments passed to a mocked function call are forwarded to Body unchanged, and can be used in return value evaluation.

    meck:expect(foo, bar, ['_'], meck:exec(fun(X) -> X*X end)),
    ?assertMatch(4, foo:bar(2)),

    This Return Specification has a less verbose counterpart which is just Body.

    meck:expect(foo, bar, ['_'], fun(X) -> X*X end),
    ?assertMatch(4, foo:bar(2)),
  • meck:seq(Sequence::[meck:ret_spec()]) Initialized with a list of Return Specifications. When it is evaluated for the first time, the first Return Specification from the list is evaluated. When it is evaluated for the second time the second Return Specification from the list is evaluated. And so on until the last Return Specification is evaluated, from that point the value evaluated by the last Return Specification in the list is returned.

    meck:expect(foo, bar, ['_'], meck:seq([a, b, c])),
    ?assertMatch(a, foo:bar(1)),
    ?assertMatch(b, foo:bar(1)),
    ?assertMatch(c, foo:bar(1)),
    ?assertMatch(c, foo:bar(1)),

    Note that Sequence may contain Return Specification of any type including meck:seq and meck:loop, that allows for rather sophisticated Return Specifications:

    meck:expect(foo, bar, ['_'], meck:seq([meck:val(a),
                                           meck:seq([b,
                                                     meck:passthrough(),
                                                     meck:raise(error, d)]),
                                           meck:exec(fun(X) -> X+X end),
                                           e])),
    ?assertMatch(a, foo:bar(1)),
    ?assertMatch(b, foo:bar(1)),
    ?assertMatch(c, foo:bar(1)), % Assume that c is what original foo:bar/1 returns
    ?assertError(d, foo:bar(1)),
    ?assertMatch(2, foo:bar(1)),
    ?assertMatch(e, foo:bar(1)),
    ?assertMatch(e, foo:bar(1)),
  • meck:loop(Loop::[meck:ret_spec()]) Same as meck:seq but as soon as the last element in Loop is reached alteration of return values does not stop, but restarts from the first Return Specification in Loop. meck:loop can also contain embedded meck:seq and meck:loop instances.

    meck:expect(foo, bar, ['_'], meck:loop([a, b, c])),
    ?assertMatch(a, foo:bar(1)),
    ?assertMatch(b, foo:bar(1)),
    ?assertMatch(c, foo:bar(1)),
    ?assertMatch(a, foo:bar(1)),

History Reset

Function meck:reset(Mod::atom()) discards all call history that has been collected for Mod. It comes handy when a lot of calls to mocked functions are made during fixture initialization, and you do not want them to interfere with testing results.

Stub All

When a mock of a module is created with meck:new(Mod::atom()) the internal mechanics of Meck fetches the Mod code from the code server and supersedes it with a code generated by Meck. At this point the generated code has no exported functions. Every time when an expectation is created by meck:expect/3 the generated code is updated to export the expected function.

Sometimes it may be convenient to make generated code export all the functions that were exported from the original Mod code. That is where stub_all option comes to the rescue. It allows you to specify a Return Specification that will be evaluated to generate a value whenever an exported function is called.

meck:new(blah, [{stub_all, meck:seq([a, b])}]), % Assuming that foo and bar are exported from blah
?assertMatch(a, blah:foo()),
?assertMatch(a, blah:bar()),
?assertMatch(b, blah:bar()),
?assertMatch(b, blah:foo()),
?assertMatch(b, blah:foo()),

The option may be specified just as stub_all that is a shorter version of {stub_all, ok}.

meck:new(blah, [stub_all]), % Assuming that foo is exported from blah
?assertMatch(a, blah:foo()),

Support for Matchers

It has already been mentioned that a matcher can be provided as an element of an arguments specification. A matcher can be either a predicate function (that is a function of arity one that returns a boolean() value) or a matcher of a supported 3rd-party framework. To distinguish a matcher from a literal value it should be wrapped by meck:is/1 function.

TooShort = fun(Str) -> erlang:length(Str) < 3 end,
meck:expect(foo, bar, [{[meck:is(TooShort)], too_short},
                       {['_', long_enough]}],
?assertMatch(too_short, foo:bar("A")),
?assertMatch(long_enough, foo:bar("This should be ok")),
Empty = fun([]) -> true;
           (_) -> false
        end,
foo:bar(""),
?assert(meck:called(foo, bar, [meck:is(Empty)])),

Hamcrest Matchers

This release brings support for Hamcrest matchers.

meck:expect(foo, bar, [{[meck:is(hamcrest_matchers:less_then(5))], ok},
                       {['_'],                                     error}],
?assertMatch(ok, foo:bar(4)),
?assertMatch(error, foo:bar(5)),
foo:bar("abracadabra"),
?assert(meck:called(foo, bar, [meck:is(hamcrest_matchers:starts_with("abra"))])),

Note that Meck does not take dependency on Hamcrest to avoid pulling of unnecessary code when it is not needed. Therefore if you do want to use Hamcrest matchers then please add the following line to the dependency list in your rebar.config:

{hamcrest, ".*", {git, "https://github.com/hyperthunk/hamcrest-erlang.git", {branch, "master"}}}

Argument Capture

This feature brings function capture that allows you to retrieve from the call history the value of a particular argument of a particular function call. The first parameter of the function identifies what of all matching calls is of interest. It can be specified as a number or either of atoms first and last. The number of an argument that should be retrieved is specified by the last function argument. If a matching function call is found in the call history then Value::term() is returned, otherwise it fails with not_found error.

This example shows how you can specify different occurrences:

test:foo(1001, 2001, 3001, 4001),
test:bar(1002, 2002, 3002),
test:foo(1003, 2003, 3003),
test:bar(1004, 2004, 3004),
test:foo(1005, 2005, 3005),
test:foo(1006, 2006, 3006),
test:bar(1007, 2007, 3007),
test:foo(1008, 2008, 3008),

?assertMatch(2003, meck:capture(first, test, foo, ['_', '_', '_'], 2)),
?assertMatch(2008, meck:capture(last, test, foo, ['_', '_', '_'], 2)),
?assertMatch(2006, meck:capture(3, test, foo, ['_', '_', '_'], 2)),
?assertError(not_found, meck:capture(5, test, foo, ['_', '_', '_'], 2)),

This example shows how different arguments specifications are used. Note that argument specification is optional and can be given as '_', then a function call of any arity will match.

test:foo(1001, 2001, 3001, 4001),
test:bar(1002, 2002, 3002),
test:foo(1003, 2003, 3003),
test:bar(1004, 2004, 3004),
test:foo(1005, 2005),
test:foo(1006, 2006, 3006),
test:bar(1007, 2007, 3007),
test:foo(1008, 2008, 3008),

?assertMatch(2001, meck:capture(first, test, foo, '_', 2)),
?assertMatch(2003, meck:capture(first, test, foo, 3, 2)),
?assertMatch(2005, meck:capture(first, test, foo, ['_', '_'], 2)),
?assertMatch(2006, meck:capture(first, test, foo, [1006, '_', '_'], 2)),
?assertMatch(2008, meck:capture(first, test, foo,
                                ['_', '_', meck:is(hamcrest_matchers:greater_than(3006))], 2))

Wait for Call

This feature brings a family of wait functions that allows you to block execution of a test until a particular mocked function is called a particular number of times by the process under test, or the specified timeout is elapsed. The number of invocation is counted starting from the most reset call of meck:reset/1 for the mocked module, or if meck:reset/1 has never been called for the mock, from its creation with meck:new.

If the matching function call has occurred the specified number of times within the specified timeout period then ok is returned, otherwise it fails with timeout.

%% Waits for any function of module `foo` to be called with any parameters for 1 second. 
meck:wait(foo, '_', '_', 1000),

%% Waits for `foo:bar/2` to be called 3 times within 5 seconds with the first argument equal to 1. 
meck:wait(3, foo, bar, [1, '_'], 5000),

Implicit New

When meck:expect/4 is called for a module that has not yet been mocked with meck:new/2, then meck:new/2 is called implicitly by meck itself. Please note that:

  • the created mock is honest, that is an attempt to create an expectation for a module that is not available on the code path still fails with not_mocked error as before. If you do need to mock a non-existent module then it should be mocked explicitly with the non_strict option;
  • the mock is created with implicit call to meck:new(<module name>, [passthrough]), therefore functions for which no expectation has been created forward to the original module counterparts.
  • implicitly created mocks has to be explicitly unloaded, but you can do that with meck:unload/0 function that unloads all heretofore mocked module. It is generally recommended to unload all over unload specific (meck:unload/1 that unloads specific modules), for that reduces syntactic noise imposed by Meck.