Giter Site home page Giter Site logo

meck's Introduction

Meck

A mocking library for Erlang

GitHub Actions Hex.pm version Hex.pm license Erlang versions hex.pm license

Features

See what's new in 0.8 Release Notes.

  • Dynamic return values using sequences and loops of static values
  • Compact definition of mock arguments, clauses and return values
  • Pass through: call functions in the original module
  • Complete call history showing calls, return values and exceptions
  • Mock validation, will invalidate mocks that were not called correctly
  • Throwing of expected exceptions that keeps the module valid
  • Throws an error when mocking a module that doesn't exist or has been renamed (disable with option non_strict)
  • Support for Hamcrest matchers
  • Automatic backup and restore of cover data
  • Mock is linked to the creating process and will unload automatically when a crash occurs (disable with option no_link)
  • Mocking of sticky modules (using the option unstick)

Examples

Here's an example of using Meck in the Erlang shell:

Eshell V5.8.4  (abort with ^G)
1> meck:new(dog, [non_strict]). % non_strict is used to create modules that don't exist
ok
2> meck:expect(dog, bark, fun() -> "Woof!" end).
ok
3> dog:bark().
"Woof!"
4> meck:validate(dog).
true
5> meck:unload(dog).
ok
6> dog:bark().
** exception error: undefined function dog:bark/0

Exceptions can be anticipated by Meck (resulting in validation still passing). This is intended to be used to test code that can and should handle certain exceptions indeed does take care of them:

5> meck:expect(dog, meow, fun() -> meck:exception(error, not_a_cat) end).
ok
6> catch dog:meow().
{'EXIT',{not_a_cat,[{meck,exception,2},
                    {meck,exec,4},
                    {dog,meow,[]},
                    {erl_eval,do_apply,5},
                    {erl_eval,expr,5},
                    {shell,exprs,6},
                    {shell,eval_exprs,6},
                    {shell,eval_loop,3}]}}
7> meck:validate(dog).
true

Normal Erlang exceptions result in a failed validation. The following example is just to demonstrate the behavior, in real test code the exception would normally come from the code under test (which should, if not expected, invalidate the mocked module):

8> meck:expect(dog, jump, fun(Height) when Height > 3 ->
                                  erlang:error(too_high);
                             (Height) ->
                                  ok
                          end).
ok
9> dog:jump(2).
ok
10> catch dog:jump(5).
{'EXIT',{too_high,[{meck,exec,4},
                   {dog,jump,[5]},
                   {erl_eval,do_apply,5},
                   {erl_eval,expr,5},
                   {shell,exprs,6},
                   {shell,eval_exprs,6},
                   {shell,eval_loop,3}]}}
11> meck:validate(dog).
false

Here's an example of using Meck inside an EUnit test case:

my_test() ->
    meck:new(my_library_module),
    meck:expect(my_library_module, fib, fun(8) -> 21 end),
    ?assertEqual(21, code_under_test:run(fib, 8)), % Uses my_library_module
    ?assert(meck:validate(my_library_module)),
    meck:unload(my_library_module).

Pass-through is used when the original functionality of a module should be kept. When the option passthrough is used when calling new/2 all functions in the original module will be kept in the mock. These can later be overridden by calling expect/3 or expect/4.

Eshell V5.8.4  (abort with ^G)
1> meck:new(string, [unstick, passthrough]).
ok
2> string:strip("  test  ").
"test"

It's also possible to pass calls to the original function allowing us to override only a certain behavior of a function (this usage is compatible with the passthrough option). passthrough/1 will always call the original function with the same name as the expect is defined in):

Eshell V5.8.4  (abort with ^G)
1> meck:new(string, [unstick, passthrough]).
ok
2> meck:expect(string, strip, fun
    ("foo") -> "bar";
    (String) -> meck:passthrough([String])
end).
ok
3> string:strip("  test  ").
"test"
4> string:strip("foo").
"bar"
5> meck:unload(string).
ok
5> string:strip("foo").
"foo"

Use

Meck is best used via Rebar 3. Add Meck to the test dependencies in your rebar.config:

{profiles, [{test, [{deps, [meck]}]}]}.

Manual Build

Meck uses Rebar 3. To build Meck go to the Meck directory and simply type:

rebar3 compile

In order to run all tests for Meck type the following command from the same directory:

rebar3 eunit

Documentation can be generated through the use of the following command:

rebar3 edoc

Test Output

Normally the test output is hidden, but if EUnit is run directly, two things might seem alarming when running the tests:

  1. Warnings emitted by cover
  2. An exception printed by SASL

Both are expected due to the way Erlang currently prints errors. The important line you should look for is All XX tests passed, if that appears all is correct.

Caveats

Global Namespace

Meck will have trouble mocking certain modules since it works by recompiling and reloading modules in the global Erlang module namespace. Replacing a module affects the whole Erlang VM and any running processes using that module. This means certain modules cannot be mocked or will cause trouble.

In general, if a module is used by running processes or include Native Implemented Functions (NIFs) they will be hard or impossible to mock. You may be lucky and it could work, until it breaks one day.

The following is a non-exhaustive list of modules that can either be problematic to mock or not possible at all:

  • erlang
  • supervisor
  • All gen_ family of modules (gen_server, gen_statem etc.)
  • os
  • crypto
  • compile
  • global
  • timer (possible to mock, but used by some test frameworks, like Elixir's ExUnit)

Local Functions

A meck expectation set up for a function f does not apply to the module- local invocation of f within the mocked module. Consider the following module:

-module(test).
-export([a/0, b/0, c/0]).

a() ->
  c().

b() ->
  ?MODULE:c().

c() ->
  original.

Note how the module-local call to c/0 in a/0 stays unchanged even though the expectation changes the externally visible behaviour of c/0:

3> meck:new(test, [passthrough]).
ok
4> meck:expect(test,c,0,changed).
ok
5> test:a().
original
6> test:b().
changed
6> test:c().
changed

Common Test

When using meck under Erlang/OTP's Common Test, one should pay special attention to this bit in the chapter on Writing Tests:

init_per_suite and end_per_suite execute on dedicated Erlang processes, just like the test cases do.

Common Test runs init_per_suite in an isolated process which terminates when done, before the test case runs. A mock that is created there will also terminate and unload itself before the test case runs. This is because it is linked to the process creating it. This can be especially tricky to detect if passthrough is used when creating the mock, since it is hard to know if it is the mock responding to function calls or the original module.

To avoid this, you can pass the no_link flag to meck:new/2 which will unlink the mock from the process that created it. When using no_link you should make sure that meck:unload/1 is called properly (for all test outcomes, or crashes) so that a left-over mock does not interfere with subsequent test cases.

Contribute

Patches are greatly appreciated! For a much nicer history, please write good commit messages. Use a branch name prefixed by feature/ (e.g. feature/my_example_branch) for easier integration when developing new features or fixes for meck.

Should you find yourself using Meck and have issues, comments or feedback please create an issue here on GitHub.

Meck has been greatly improved by many contributors!

Donations

If you or your company use Meck and find it useful, a sponsorship or donations are greatly appreciated!

Sponsor on GitHub Donate using Liberapay

meck's People

Contributors

aronisstav avatar beapirate avatar cmeiklejohn avatar daha avatar dszoboszlay avatar eproxus avatar horkhe avatar jesperes avatar jfacorro avatar kianmeng avatar legoscia avatar lemenkov avatar loissotolopez avatar massemanet avatar mattvonvielen avatar mbbx6spp avatar michaelklishin avatar michalwski avatar mietek avatar myers avatar nablaa avatar norton avatar paulo-ferraz-oliveira avatar pergu avatar rlipscombe avatar russelldb avatar rzezeski avatar wardbekker avatar yamt avatar zsoci avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

meck's Issues

Implicit new

This means get rid of the {not_mocked, Module} error and just run new if there is no Meck process already.

meck:validate/1 always returns true

To reproduce:

meck:new(dog).
meck:expect(dog, bark, fun() -> "Woof!" end).
meck:validate(dog).

validate returns true but the expected value is false because the bark function hasn't been called yet.

meck:unload/0 sometimes crashes

Every now and then, meck:unload/0 crashes with not_mocked for me. It seems to be a race condition: unload/0 passes everything in registered() that locks like a mock to unload_if_mocked, but if the mock module has already been unloaded at that point (due to having been mocked by a now dead process), a not_mocked error is raised and the remaining modules are not unmocked.

I could reproduce it with this Proper property (because it runs the test enough times to make the race condition more likely):

-module(foo).

-compile(export_all).
-include_lib("proper/include/proper.hrl").

prop_unload() ->
    ?FORALL({Wait1, Wait2}, {nat(), nat()},
            begin
                {_, Ref} = spawn_monitor(fun() ->
                                                 timer:sleep(Wait1),
                                                 meck:new(foobar)
                                         end),
                timer:sleep(Wait1 + Wait2),
                meck:unload(),
                receive {'DOWN', Ref, process, _Pid, _Reason} -> ok end,
                true
            end).

Failure looks like this:

22> proper:quickcheck(foo:prop_unload()).
..
=ERROR REPORT==== 4-Nov-2011::14:16:36 ===
Error in process <0.1056.0> with exit value: {{already_started,<0.1054.0>},[{meck,new,[foobar,[]]}]}

......!
Failed: After 9 test(s).
An exception was raised: error:{not_mocked,foobar}.
Stacktrace: [{meck,gen_server,3},
             {meck,unload,1},
             {meck,unload_if_mocked,2},
             {lists,foldl,3},
             {foo,'-prop_unload/0-fun-1-',1}].
{4,4}

Shrinking .(1 time(s))
{1,4}
false

This patch seems to fix the problem:

diff --git a/meck/src/meck.erl b/meck/src/meck.erl
--- a/meck/src/meck.erl
+++ b/meck/src/meck.erl
@@ -400,7 +400,8 @@ unload_if_mocked(P, L) when length(P) > 5 ->
     case lists:split(length(P) - 5, P) of
         {Name, "_meck"} ->
             Mocked = list_to_existing_atom(Name),
-            unload(Mocked),
+            catch call(Mocked, stop),
+            wait_for_exit(Mocked),
             [Mocked|L];
         _Else ->
             L

Unable to mock lists module

1> meck:new(lists, [passthrough, unstick]).

Crash dump was written to: erl_crash.dump
The code server called the unloaded module `lists'

Wait for a number of function calls

It would be great if we could wait for a particular function of a mocked module to be called a particular number of times with particular parameters. E.g.

meck:new(some_mod, [passthrough]),
....
meck:wait_4_call(3, {some_mod, f1, [blah, '_', 10]}, 10000).

where

-spec wait_4_call(CalledTimes :: non_neg_integer(),
                  {module(), func(), args_pattern()},
                  Timeout :: erlang:timeout()) ->
        ok.

The wait_4_call should fail if the call pattern, that we are waiting upon, is not met within the time specified by Timeout.

I personally need this capability to use in my functional tests to get rid of ugly timer:sleep/1.

Export access to original function in passthrough mode

Hi, sorry I didn't cook up a pull request the usual way. This is a really small change, but it's tremendously useful. When a module is defined in passthrough mode, when doing the following:

meck:new(foo, [passthrough]),
meck:expect(foo, bar, fun(X) case random:uniform(100) of
    N when N < 50 ->
        %% somehow call the original foo:bar/1 function but with
        %% the same X or perhaps a modified X.
    _ ->
        %% Do something completely novel, like we usually
        %% do with Meck.
    end).

Using this new function, we could put the following into the first case clause:

meck:orig_apply(foo, bar, [X]);

or perhaps:

meck:orig_apply(foo, bar, [modify_somehow(X)]);

The small patch is:

diff --git a/src/meck.erl b/src/meck.erl
index 1669c32..6f8a577 100644
--- a/src/meck.erl
+++ b/src/meck.erl
@@ -40,6 +40,7 @@
 -export([called/4]).
 -export([num_calls/3]).
 -export([num_calls/4]).
+-export([orig_apply/3]).

 %% Callback exports
 -export([init/1]).
@@ -455,6 +456,9 @@ proc_name(Name) -> list_to_atom(atom_to_list(Name) ++ "_meck").

 original_name(Name) -> list_to_atom(atom_to_list(Name) ++ "_meck_original").

+orig_apply(Name, Func, Args) ->
+    erlang:apply(original_name(Name), Func, Args).
+
 wait_for_exit(Mod) ->
     MonitorRef = erlang:monitor(process, proc_name(Mod)),
     receive {'DOWN', MonitorRef, _Type, _Object, _Info} -> ok end.

Thanks for considering. I'm happily using this new function for nefarious testing purposes. The rest of Meck is quite useful, thanks for all of your work.

Use right function in exceptions

Rewrite exception handling so that exceptions look like they come from the mocked module.

1> meck:new(test, [no_link]).
ok
2> Foo = fun(A) when is_integer(A) -> bar end.
#Fun<erl_eval.6.80247286>
3> meck:expect(test, foo, Foo).
ok
4> test:foo(a).
** exception error: no function clause matching
                    erl_eval:'-inside-an-interpreted-fun-'(a)

Exception should look like the following:

4> test:foo(a).
** exception error: no function clause matching
                    test:foo(a)

meck:unload is undefined?

Using this module, and R15B01:

-module(example).
-compile(export_all).

get_config() ->
    mocked:myfun("test.cfg").

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

get_config_test_() ->
    meck:new(mocked),
    meck:expect(mocked, myfun, fun(Filename) -> ok end),
    ?_assertEqual(ok, get_config()),
    ?_assert(meck:validate(mocked)),
    meck:unload(mocked).
-endif.

Compiling and running tests:

1> c(example, [{d, 'TEST'}]).
example.erl:19: Warning: variable 'Filename' is unused
example.erl:20: Warning: a term is constructed, but never used
example.erl:21: Warning: a term is constructed, but never used
{ok,example}
12> example:test().           
undefined
*** test module not found ***
::ok

=======================================================
  Failed: 0.  Skipped: 0.  Passed: 0.
One or more tests were cancelled.
error

However, if I comment the line meck:unload(mocked). then test passes successfully.

Mocking of sticky modules

Handle mocking of sticky modules (possible today, if unsticked first, but not for modules used by the code server).

  • Possible to solve the code server issues with erlang:load_module/2? :-)

meck:app

This is a feature request for an new function which mocks an application so that you don't have to start all application dependencies when testing an application. i.e.

meck:app(stdlib).
ok = application:start(ssl).

Finalize 0.8 release

I'd like to finalize and tag the 0.8 release. Right now only #14 is blocking this release.

Since the API does change a little, I'm contemplating to name this 1.0 instead so people should not expect a totally smooth update.

TODO:

  • Fix/close #14
  • Decide 0.8 vs 1.0

Add test/cover_test_module.beam to rebar.config's clean files

Running eunit and then dialyzer triggers the following error below.

Can you consider adding this file to rebar.config's clean files?

{clean_files, ["test/cover_test_module.beam"]}.

==> src (compile)
dialyzing w/spec: ...
dialyzer --plt /Users/norton/.dialyzer_plt.R14B04 -Wunmatched_returns -r ./lib | fgrep -v -f dialyze-ignore-warnings.txt
Checking whether the PLT /Users/norton/.dialyzer_plt.R14B04 is up-to-date... yes
Compiling some key modules to native code... done in 0m47.91s
Proceeding with analysis...
dialyzer: Analysis failed with error: Could not scan the following file(s): [{"src/lib/meck/test/cover_test_module.beam",
" Could not get abstract code for: src/lib/meck/test/cover_test_module.beam\n Recompile with +debug_info or analyze starting from source code"}]
Last messages in the log cache:
Reading files and computing callgraph...

Makefile requires local rebar and documentation says rebar on path

The README.md states

meck requires rebar to build. Either install rebar by building it manually and putting it in your
path or by using Agner (agner install rebar).

But when I put the rebar script on my path I get the following:

make: ./rebar: No such file or directory
make: **\* [all] Error 1

Running

in the meck directory works like a charm.

Am I missing something here?

This was done on Mac OS X 10.6.7 using Erlang R14B02.

Cheers,
Torben

Mocking of parameterized modules

It should be possible to mock parameterized modules.

Investigate what happens today if a parameterized module is mocked, or how it would work.

dialyzer unmatched return errors

Using dialyzer's unmatched return analysis, the following dialyzer errors are reported for meck.

meck.erl:77: Expression produces a value of type ['ok'], but this value is unmatched
meck.erl:103: Expression produces a value of type ['ok'], but this value is unmatched
meck.erl:121: Expression produces a value of type ['ok'], but this value is unmatched
meck.erl:138: Expression produces a value of type ['ok'], but this value is unmatched
meck.erl:153: Expression produces a value of type ['ok'], but this value is unmatched
meck.erl:220: Expression produces a value of type ['ok'], but this value is unmatched
meck.erl:496: Expression produces a value of type 'ok' | {'error',atom()}, but this value is unmatched

Inconsistency in documentation

everywhere in the documentation (README/wiki/etc) there's 'nolink' parameter used in examples, but the correct one (according to meck.erl) is 'no_link', I spent about an hour before I made myself look at the source code. It would be great to have the documentation fixed (not for me of course, but for other new guys who would try to use meck :))

thanks in advance!

dialyzer warnings with meck (73c0b3e)

I noticed a few new dialyzer warnings.

[meck ((73c0b3e...))]$ dialyzer --version
Dialyzer version v2.5

[meck ((73c0b3e...))]$ dialyzer --plt ~/.dialyzer_plt.R15B -Wunmatched_returns --src src
Checking whether the PLT /Users/norton/.dialyzer_plt.R15B is up-to-date... yes
Proceeding with analysis...
meck.erl:356: Expression produces a value of type binary(), but this value is unmatched
meck.erl:521: Expression produces a value of type binary(), but this value is unmatched
meck.erl:653: The pattern <Mod, Func, Args, [Meck = {'meck', 'exec', Arity} | Stack]> can never match the type <atom(),,_,[{atom(),atom(),[any()] | byte(),[any()]},...]>
meck_cover.erl:29: Expression produces a value of type 'ok' | binary(), but this value is unmatched
meck_cover.erl:30: Call to missing or unexported function cover:compile_beam/2
meck_cover.erl:93: Expression produces a value of type 'ok' | {'error',atom()}, but this value is unmatched
meck_cover.erl:97: Call to missing or unexported function cover:get_term/1
meck_cover.erl:104: Expression produces a value of type [any()], but this value is unmatched
meck_cover.erl:108: Call to missing or unexported function cover:write/2
done in 0m15.09s
done (warnings were emitted)

[meck ((73c0b3e...))]$ git log -n 1
commit 73c0b3e
Merge: 0e1b928 e84d4d0
Author: Adam Lindberg [email protected]
Date: Fri Mar 9 17:00:14 2012 -0800

Merge pull request #57 from Erkan-Yilmaz/master

Remove repetition and typo in documentation

Eunit test process dies on purge of meck module

I have an issue with some Eunit tests that don't run because of the code:purge/1 step when unloading a module. I've recreated the problem below and it's also available at https://github.com/bjnortier/meck-issue

When I run the eunit tests with rebar the test process terminates:

$ rebar -v skip_deps=true compile eunit
==> meck-issue (compile)
==> meck-issue (eunit)
======================== EUnit ========================
a:8: a_test_ (module 'a')...ok
module 'b'
  b:16: b_test_...ok
undefined
*unexpected termination of test process*
::killed

Failed: 0. Skipped: 0. Passed: 2.
One or more tests were cancelled.
ERROR: One or more eunit tests failed.

Which I have traced to the code:purge/1 function when unloading a module. I suspect it's because of the following bit in the docs for code:purge/1:
"If some processes still linger in the old code, these processes are killed before the code is removed."

Module a is tested first, which loads the real a. Then the tests for module b mocks/mecks module a, which is already loaded. So ordering is important.

Perhaps someone with more knowledge of code loading/unloading can help.
Here are the two modules:

-module(a).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

a_test_() ->
    [
     ?_assert(true)
    ].

-endif.


-module(b).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

b_test_() ->
    {setup,
     fun() ->
             ok = meck:new(a)
     end,
     fun(_) -> 
             meck:unload(a)
     end,
     fun(_) ->
             [
              ?_assert(true)
             ]
         end
    }.

-endif.

Capture argument

It would be nice to have an ability to capture the argument that was passed as particular function parameter during a test. I suggest the following API:

-spec capture(CallNumber:: first | last | all | pos_number(),
              Mod::atom(),
              Func::atom(),
              ParameterNumber::byte()) ->
        any() | [any()]

A list of values is returned if the first meck:capture/4 parameter is all.

Passthrough crashes without +debug_info

When meck:new(Mod, [passthrough]) is called to mock a module that is not compiled with +debug_info it will still return ok.

When a call is made to a function in that module it crashes:

1> meck:new(mymod, [passthrough]).
ok
2> mymod:function().
** exception error: undefined function mymod_original:function/0
     in function  meck:exec/4
     in call from mymod:function/0
        called as mymod:function()

The fault is that the renamed original was never created.

meck eunit tests fail on R15B

I tried R15B today for the first time and found that meck eunit tests are failing with R15B.

I'm guessing this is a new issue due to line numbers added in R15B.

make test
==> meck (eunit)
Compiled test/meck_performance_test.erl
Compiled test/meck_test_module.erl
Compiled src/meck_mod.erl
Compiled src/meck.erl
Compiled test/meck_tests.erl
meck_tests: stacktrace_...failed
::{assertion_failed,[{module,meck_tests},
{line,171},
{expression,"lists : any ( fun ( { M , test , [ ] } ) when M == Mod -> true ; ( _ ) -> false end , erlang : get_stacktrace ( ) )"},
{expected,true},
{value,false}]}

meck_tests: stacktrace_function_clause_...failed
::{assertion_failed,[{module,meck_tests},
{line,184},
{expression,"lists : any ( fun ( { M , test , [ error ] } ) when M == Mod -> true ; ( _ ) -> false end , Stacktrace )"},
{expected,true},
{value,false}]}

meck_tests: history_error_args_...failed
::{assertion_failed,[{module,meck_tests},
{line,257},
{expression,"lists : any ( fun ( { _M , _F , [ fake_args ] } ) -> true ; ( _ ) -> false end , Stacktrace )"},
{expected,true},
{value,false}]}

=ERROR REPORT==== 19-Dec-2011::00:36:59 ===
** Generic server mymod_meck terminating
** Last message in was {'EXIT',<0.97.0>,expected_test_exit}
** When Server state == {state,mymod,
{dict,0,16,16,8,80,48,
{[],[],[],[],[],[],[],[],[],[],[],[],[],
[],[],[]},
{{[],[],[],[],[],[],[],[],[],[],[],[],[],
[],[],[]}}},
true,[],false,false}
** Reason for termination ==
** expected_test_exit

=ERROR REPORT==== 19-Dec-2011::00:37:00 ===

Can't load module that resides in sticky dir

Failed: 3. Skipped: 0. Passed: 82.
Cover analysis: /Users/norton/tmp/meck/.eunit/index.html
Node State Type In Out Address

=ERROR REPORT==== 19-Dec-2011::00:37:01 ===
** Generic server inet_gethost_native_sup terminating
** Last message in was {'EXIT',<0.644.0>,killed}
** When Server state == {state,inet_gethost_native,undefined,<0.644.0>,
{local,inet_gethost_native_sup}}
** Reason for termination ==
** killed
ERROR: One or more eunit tests failed.

Interface inconsistency

Fix meck API exceptions to be thrown from inteface functions with erlang:error/2 instead of throw (in some rare cases).

Mock a module only from the perspective of a specific module

It should be possible to mock a module only from the perspective of another specific module. This would allow mocking the file module for example, which is not possible due to its use from the code module (resulting in a crash when replacing the module).

Implementation Proposal

Instead of replacing and compiling the mock module globally, a mocked module is created with another name than the original. Then all calls to the original module in the module under test is replaced with calls to the new mocked module.

Unload all feature

For easy cleanup in between test cases I would like to have an meck:unload() function which unloads all mocked modules.

meck:new with passthrough

meck:new does not report when you try to use passthrough on a module which is not compiled with +debug_info

c(mymod).
ok = meck:new(mymod, [passthrough]).
mymod:foo().

meck:new returns ok, but mymod:foo crashed.

Can I call original function with different arguments?

I tried to do that:

meck:unstick_mod(string),
meck:new(string),
meck:expect(string, strip, fun(String) -> meck:passthrough(["   foo   "]) end),
string:strip("   test  ").

I've expected "foo" as a result but I've got an exception: {{case_clause, {passthrough, [" foo "]}}, [{meck, handle_mock_exception, 4}, ... ]}

Just a question about Hostname in remote_setup()

Hi,
Sorry that this is probably not an issue with the code, but there is no mailing list so this seems like the best place to ask:

I started using meck yesterday. I'm running OS X 10.6.4. Erlang R14B. When I run the test suite using
./rebar eunit

I get an exception
{error, ecoonnrefused}

from

 Myself = list_to_atom("meck_eunit_test@" ++ Hostname),
 net_kernel:start([Myself, shortnames]),

If, however, I remove the "@hostname" from the nodename it works fine.
Myself = list_to_atom("meck_eunit_test"),

I don't understand why. If you have the time can you explain it to me and help me figure out how to set up my machine so the test passes without my change to the code?

Cheers

Russell

Add helper function to mock modules

Implement a helper function which does the call to meck itself. Make all generated mock functions use that helper function.

This will have several benefits:

  • Possible to implement clauses from the original module in the mock module (they all will use the helper function).
  • Insert line number and file information from original module into the mock (for exception injection)

meck:new fails if running in embedded mode and module not loaded

When running erlang in embedded mode, meck:new fails if the (original) module exists, but isn't loaded.

How to repeat

  1. Compile to following module:

$ erlc +debug_info test.erl

  1. Start Erlang in embbedded mode and load all the modules needed by meck, and mock the test module:
    $ erl -mode embedded -pa meck/ebin/ -pa . -pa PATH_TO_ERL_INSTALL/lib//ebin/
    application:start(compiler).
    {ok, Modules} = application:get_key(compiler, modules).
    [ l(M) || M <- Modules ++ [meck, meck_mod, cover] ].
    4> meck:new(test).
    *
    exception exit: undef
    in function test:module_info/1
    called as test:module_info(compile)
    in call from meck_mod:compile_options/1
    in call from meck:backup_original/1
    in call from meck:init/1
    in call from gen_server:init_it/6
    in call from proc_lib:init_p_do_apply/3

Solution

Normally, in embedded mode, one doesn't meck a module that isn't. Thus, in practice the problem occurs when the module is first loaded and when running the sequence: meck:new(test), meck:unload(test), meck:new(test), since meck:unload doesn't load the original module, but rather relies on the code server auto-loading it.

  • A potential fix would be to call code:ensure_loaded in meck:unload, and call code:load_file if that returns {error, embedded}. Since the original module doesn't have to exist, any errors from code:load_file would have to be ignored though.
  • Perhaps meck:new should call code:ensure_loaded in the function backup_original, and not backup the original if the result is {error, embedded}.

R16A preview - parameterized modules are no longer supported

I briefly checked if meck can build or not with a preview version of R16A. Parameterized modules will be going away soon. Have you any ideas so far on how to handle this test case? delete it, define macro, etc.

meck/test/meck_test_parametrized_module.erl:1: parameterized modules are no longer supported
meck/test/meck_test_parametrized_module.erl:6: variable 'Var1' is unbound
meck/test/meck_test_parametrized_module.erl:7: variable 'Var2' is unbound

erl --version
Erlang R16A (erts-5.10) [source] [64-bit] [smp:2:2] [async-threads:10] [kernel-poll:false]

Eshell V5.10  (abort with ^G)
1> 

Coverage reports lost

Hi,
I am running Erlang R13B. I have an Erlang application with several modules and unit tests. Each test mocks all the application modules but the one it actually test. After running all the tests together I don't have the coverage information for all of them, since when the mocked modules are loaded the coverage information is lost.

Build failed on mips and sh4 at Debian

Hi,

meck build failed on mips and sh4 at Debian.

Build log following;
mips:
https://buildd.debian.org/status/fetch.php?pkg=erlang-meck&arch=mips&ver=0.7.2-2&stamp=1349831619
sh4:
http://buildd.debian-ports.org/status/fetch.php?pkg=erlang-meck&arch=sh4&ver=0.7.2-2&stamp=1349841585

These architecture seems to be an error in the same part.


meck_tests: sequence_multi_...[0.128 s] ok
meck_tests: loop_...[0.051 s] ok
meck_tests: loop_multi_...[0.154 s] ok
meck_tests: call_original_test...[0.125 s] ok
meck_tests: unload_renamed_original_test...[0.064 s] ok
meck_tests: unload_all_test...[0.325 s] ok
meck_tests: original_no_file_test...[0.089 s] ok
meck_tests: original_has_no_object_code_test...[0.047 s] ok
meck_tests: passthrough_nonexisting_module_test...[0.172 s] ok
meck_tests: passthrough_test...[0.130 s] ok
meck_tests: passthrough_different_arg_test...[0.091 s] ok
meck_tests: passthrough_bif_test...[2.473 s] ok
meck_tests: cover_test...*timed out*

undefined

Failed: 0. Skipped: 0. Passed: 64.

Would you give me the advice for correcting about this problem?
If it is an error by timeout, Would you teach me how to extend the time of timeout?

Thanks,
Nobuhiro

crypto module

Hi,

I'm trying to mock the crypto module just to check if it gets called but an error is raised instead.

This is the error output:

9> meck:new(crypto).               

=ERROR REPORT==== 22-Mar-2012::18:53:02 ===
Unable to load crypto library. Failed with error:
"bad_lib, Library module name 'crypto' does not match calling module 'crypto_meck_original'"
OpenSSL might not be installed on this system.

=ERROR REPORT==== 22-Mar-2012::18:53:02 ===
The on_load function for module crypto_meck_original returned {error,
                                                               {bad_lib,
                                                                "Library module name 'crypto' does not match calling module 'crypto_meck_original'"}}
** exception exit: {error_loading_module,crypto_meck_original,on_load_failure}

Mocking other modules works perfectly.

Split tests into several test suites

The test module is too large. It should be split into several test suites. Maybe grouped by API functions (basic scenarios, passthrough, etc.)?

Mocking of 'file' crashes file_meck

Using this module, and R15B01.

-module(example).
-compile(export_all).

get_config() ->
    file:consult("test.cfg").

-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").

get_config_test_() ->
    meck:new(file, [unstick, passthrough]),
    meck:expect(file, consult, fun(Filename) -> ok end),
    ?_assertEqual(ok, get_config()),
    ?_assert(meck:validate(file)),
    meck:unload(file).
-endif.

file_meck crashes when running the tests:

1> c(example, [{d, 'TEST'}]).
example.erl:19: Warning: variable 'Filename' is unused
example.erl:20: Warning: a term is constructed, but never used
example.erl:21: Warning: a term is constructed, but never used
{ok,example}
2> example:test().
undefined
*** test generator failed ***
::{not_mocked,file}


=======================================================
  Failed: 0.  Skipped: 0.  Passed: 0.
One or more tests were cancelled.
error
11> 
=ERROR REPORT==== 22-Jul-2012::19:05:31 ===
** Generic server file_meck terminating 
** Last message in was {expect,consult,#Fun<example.0.68874524>}
** When Server state == {state,file,
                               {dict,63,16,16,8,80,48,
                                     {[],[],[],[],[],[],[],[],[],[],[],[],[],
                                      [],[],[]},
                                     {{[[{pid2name,1}|passthrough],
                                        [{altname,1}|passthrough],
                                        [{read_file,1}|passthrough],
                                        [{read_line,1}|passthrough],
                                        [{truncate,1}|passthrough],
                                        [{eval,2}|passthrough],
                                        [{path_eval,2}|passthrough],
                                        [{delete,1}|passthrough],
                                        [{close,1}|passthrough]],
                                       [[{pread,3}|passthrough],
                                        [{ipread_s32bu_p32bu_int,3}|
                                         passthrough],
                                        [{path_script,3}|passthrough],
                                        [{copy_opened,3}|passthrough]],
                                       [[{write_file,3}|passthrough],
                                        [{pwrite,3}|passthrough],
                                        [{ipread_s32bu_p32bu,3}|passthrough],
                                        [{change_time,3}|passthrough],
                                        [{position,2}|passthrough],
                                        [{open,2}|passthrough]],
                                       [[{read_file_info,2}|passthrough],
                                        [{read_link_info,2}|passthrough],
                                        [{write_file_info,2}|passthrough],
                                        [{raw_write_file_info,2}|passthrough]],
                                       [[{change_group,2}|passthrough],
                                        [{sendfile,5}|passthrough]],
                                       [],
                                       [[{read_link,1}|passthrough],
                                        [{copy,3}|passthrough],
                                        [{change_owner,2}|passthrough],
                                        [{get_cwd,0}|passthrough]],
                                       [[{eval,1}|passthrough]],
                                       [[{pread,2}|passthrough],
                                        [{path_consult,2}|passthrough],
                                        [{script,2}|passthrough],
                                        [{path_script,2}|passthrough],
                                        [{read,2}|passthrough]],
                                       [[{rename,2}|passthrough],
                                        [{write_file,2}|passthrough],
                                        [{pwrite,2}|passthrough],
                                        [{path_eval,3}|passthrough],
                                        [{change_mode,2}|passthrough],
                                        [{change_time,2}|passthrough],
                                        [{sendfile,2}|passthrough],
                                        [{write,2}|passthrough]],
                                       [[{read_link_info,1}|passthrough],
                                        [{raw_read_file_info,1}|passthrough],
                                        [{read_file_info,1}|passthrough]],
                                       [[{advise,4}|passthrough],
                                        [{path_open,3}|passthrough]],
                                       [[{write_file_info,3}|passthrough]],
                                       [[{make_dir,1}|passthrough],
                                        [{del_dir,1}|passthrough],
                                        [{copy,2}|passthrough],
                                        [{list_dir,1}|passthrough],
                                        [{format_error,1}|passthrough]],
                                       [[{datasync,1}|passthrough],
                                        [{sync,1}|passthrough]],
                                       [[{set_cwd,1}|passthrough],
                                        [{make_link,2}|passthrough],
                                        [{make_symlink,2}|passthrough],
                                        [{script,1}|passthrough],
                                        [{change_owner,3}|passthrough],
                                        [{get_cwd,1}|passthrough],
                                        [{consult,1}|passthrough]]}}},
                               true,[],
                               {false,no_passthrough_cover},
                               true}
** Reason for termination == 
** {compile_forms,
       {error,
           [{[],
             [{none,compile,
                  {crash,beam_asm,
                      {{not_mocked,file},
                       [{meck,gen_server,3,[{file,"src/meck.erl"},{line,452}]},
                        {meck,exec,5,[{file,"src/meck.erl"},{line,417}]},
                        {filename,absname,1,[{file,"filename.erl"},{line,67}]},
                        {compile,beam_asm,1,
                            [{file,"compile.erl"},{line,1241}]},
                        {compile,'-internal_comp/4-anonymous-1-',2,
                            [{file,"compile.erl"},{line,269}]},
                        {compile,fold_comp,3,
                            [{file,"compile.erl"},{line,287}]},
                        {compile,internal_comp,4,
                            [{file,"compile.erl"},{line,271}]},
                        {compile,internal,3,
                            [{file,"compile.erl"},{line,246}]}]}}}]}],
           []}}

Deprecate history and provide history digging functions instead

Test writers do not need entire history. They need a convenient way to find out some specific facts about the history like called and num_calls functions. By providing them entire history we kind of forcing them to write custom information retrieval and verification functions.

I believe a better way would be to provide a comprehancive set of history digging functions, such as:

  • verify - a unified version of called and num_calls that allows complex number of calls verification, argument matching using standard Erlang patterns and possibly Hamcrest matchers. It will also support verification of the order in which functions were called;
  • capture - provides a way to retrieve a particular argument value that a particular function was called with.

That probably covers everything a testing human being might need.

Mock a module only from the perspective of a specific caller (calling process)

It should be possible to mock a module so that a set of processes sees the mocked module and all other processes sees the original module.

  • mock:new(mymod, [{restrict, pid()}]):
    • only the caller pid() would hit the mocked module
  • or even better, all pids belonging to a specific application
  • Would enable mocking of the inet, gen_tcp modules etc

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.