Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Introduce 'stub_all' option #78

Merged
merged 3 commits into from

2 participants

Maxim Vladimirsky Adam Lindberg
Maxim Vladimirsky

For those who use mocks in record-then-verify manner (and I am one of them :-)) it would be very convenient to create mocks that have stubs for all exported functions out-of-the-box. This feature introduces such capability. The default stubs allow everything as input arguments and return meck_stub. If in the scope of a test a more specific stub (expectation) is required, then the out-of-the-box stub (expectation) can be overridden by explicit call to meck:expect/3,4.

E.g. If there is a module m1 that exports function f/2 then:

meck:new(m1, [stub_all]),
?assertEquals(meck_stub, m1:f(blah, whatever)).
Adam Lindberg
Owner

Might I suggest a few changes?

  • Default return value should be ok (more Erlang-ish)
  • The option could handle {stub_all, ReturnValue} or even {stub_all, Fun} (tip: use proplists:expand/2 and family)
Adam Lindberg

The case statement can be rewritten as follows:

case Exports of
    undefined ->
        [];
    Exports when Passthrough ->
        [passthrough_stub(FuncArity) || FuncArity <- Exports];
    Exports when StubAll ->
        [void_stub(FuncArity) || FuncArity <- Exports];
    _ ->
        []
end

I think it is a little more cleaner, what do you think?

Adam Lindberg

val(meck_stub) only? (without meck:)

Maxim Vladimirsky

It is a neat idea to allow defining values returned by stubs. I just implemented it in a more general way. Any valid ret_spec() can be specified as a return value of the stub_all option.

Adam Lindberg

There's probably a bug here when someone uses {stub_all, undefined}, should probably use proplists:is_defined/2 for this. Please also add a test case for this particular scenario.

Adam Lindberg
Owner

Alternatively we have to expand the stub_all option to {stub_all, ok} first, and then use that in combination with proplists:is_defined(stub_all, Options) to see if we can trust the return value of proplists:get_value(stub_all, Options) or not.

Maxim Vladimirsky

Fixed this case too.

Maxim Vladimirsky

I wonder, why my pull request does not have that nice green label from Travis-CI that it is ok to merge...

Adam Lindberg
Owner

Nice work!

Adam Lindberg eproxus merged commit a6e1553 into from
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Oct 12, 2012
  1. Introduce 'stub_all' option

    Maxim Vladimirsky authored
Commits on Oct 22, 2012
  1. Maxim Vladimirsky
Commits on Oct 23, 2012
  1. Fix {stub_all, undefined} and {stub_all, true} cases

    Maxim Vladimirsky authored
This page is out of date. Refresh to see the latest.
Showing with 115 additions and 60 deletions.
  1. +83 −60 src/meck.erl
  2. +32 −0 test/meck_tests.erl
143 src/meck.erl
View
@@ -85,7 +85,8 @@
%% Opaque data structure that defines values to be returned by expectations.
%% Values of `ret_spec' are constructed by {@link seq/1}, {@link loop/1},
%% {@link val/1}, and {@link raise/2} functions. They are used to specify
-%% return values in {@link expect/3} and {@link expect/4} functions.
+%% return values in {@link expect/3} and {@link expect/4} functions, and also
+%% as the parameter of the `stub_all' option of {@link new/2} function.
-opaque ret_spec() :: {meck_val, term()} |
{meck_seq, [term()]} |
{meck_loop, [term()], [term()]} |
@@ -129,31 +130,41 @@ new(Mod) when is_list(Mod) -> lists:foreach(fun new/1, Mod), ok.
%%
%% The valid options are:
%% <dl>
-%% <dt>`passthrough'</dt><dd>Retains the original functions, if not
-%% mocked by meck.</dd>
-%% <dt>`no_link'</dt> <dd>Does not link the meck process to the caller
-%% process (needed for using meck in rpc calls).
-%% </dd>
-%% <dt>`unstick'</dt> <dd>Unstick the module to be mocked (e.g. needed
-%% for using meck with kernel and stdlib modules).
-%% </dd>
-%% <dt>`no_passthrough_cover'</dt><dd>If cover is enabled on the module to be
-%% mocked then meck will continue to
-%% capture coverage on passthrough calls.
-%% This option allows you to disable that
-%% feature if it causes problems.
-%% </dd>
-%% <dt>`{spawn_opt, list()}'</dt><dd>Specify Erlang process spawn options.
-%% Typically used to specify non-default,
-%% garbage collection options.
-%% </dd>
-%% <dt>`no_history'</dt> <dd>Do not store history of meck calls.
-%% </dd>
-%% <dt>`non_strict'</dt><dd>A mock created with this option will allow setting
-%% expectations on functions that are not exported
-%% from the mocked module. With this option on it is
-%% even possible to mock non existing modules.
-%% </dd>
+%% <dt>`passthrough'</dt>
+%% <dd>Retains the original functions, if not mocked by meck. If used along
+%% with `stub_all' then `stub_all' is ignored.</dd>
+%%
+%% <dt>`no_link'</dt>
+%% <dd>Does not link the meck process to the caller process (needed for using
+%% meck in rpc calls).</dd>
+%%
+%% <dt>`unstick'</dt>
+%% <dd>Unstick the module to be mocked (e.g. needed for using meck with
+%% kernel and stdlib modules).</dd>
+%%
+%% <dt>`no_passthrough_cover'</dt>
+%% <dd>If cover is enabled on the module to be mocked then meck will continue
+%% to capture coverage on passthrough calls. This option allows you to
+%% disable that feature if it causes problems.</dd>
+%%
+%% <dt>`{spawn_opt, list()}'</dt>
+%% <dd>Specify Erlang process spawn options. Typically used to specify
+%% non-default, garbage collection options.</dd>
+%%
+%% <dt>`no_history'</dt>
+%% <dd>Do not store history of meck calls.</dd>
+%%
+%% <dt>`non_strict'</dt>
+%% <dd>A mock created with this option will allow setting expectations on
+%% functions that are not exported from the mocked module. With this
+%% option on it is even possible to mock non existing modules.</dd>
+%%
+%% <dt>`{stub_all, '{@link ret_spec()}`}'</dt>
+%% <dd>Stubs all functions exported from the mocked module. The stubs will
+%% return whatever defined by {@link ret_spec()} regardless of arguments
+%% passed in. It is possible to specify this option as just `stub_all'
+%% then stubs will return atom `ok'. If used along with `passthrough'
+%% then `stub_all' is ignored. </dd>
%% </dl>
-spec new(Mod:: atom() | [atom()], Options::[term()]) -> ok.
new(Mod, Options) when is_atom(Mod), is_list(Options) ->
@@ -429,7 +440,7 @@ reset(Mods) when is_list(Mods) ->
%% values. It is intended to be in construction of clause specs for the
%% {@link expect/3} function.
%%
-%% Calls to an expect, created with {@link ret_spec} returned by this function,
+%% Calls to an expect, created with {@link ret_spec()} returned by this function,
%% will return one element at a time from the `Loop' list and will restart at
%% the first element when the end is reached.
-spec loop(Loop::[term()]) -> ret_spec().
@@ -471,19 +482,7 @@ raise(exit, Reason) -> {meck_raise, exit, Reason}.
%% @hidden
init([Mod, Options]) ->
- case proplists:get_bool(non_strict, Options) of
- true ->
- init([Mod, Options, any]);
- _ ->
- try
- Exports = Mod:module_info(exports),
- init([Mod, Options, Exports])
- catch
- error:undef ->
- {stop, undefined_module}
- end
- end;
-init([Mod, Options, CanExpect]) ->
+ Exports = normal_exports(Mod),
WasSticky = case proplists:get_bool(unstick, Options) of
true -> {module, Mod} = code:ensure_loaded(Mod),
unstick_original(Mod);
@@ -493,8 +492,9 @@ init([Mod, Options, CanExpect]) ->
Original = backup_original(Mod, NoPassCover),
NoHistory = proplists:get_bool(no_history, Options),
History = if NoHistory -> undefined; true -> [] end,
+ CanExpect = resolve_can_expect(Exports, Options),
+ Expects = init_expects(Exports, Options),
process_flag(trap_exit, true),
- Expects = init_expects(Mod, Options),
try
_Bin = meck_mod:compile_and_load_forms(to_forms(Mod, Expects)),
{ok, #state{mod = Mod,
@@ -513,12 +513,11 @@ handle_call({get_expect, FuncAri, Args}, _From, S) ->
{Expect, NewExpects} = get_expect(S#state.expects, S#state.mod, FuncAri,
Args),
{reply, Expect, S#state{expects = NewExpects}};
-handle_call({expect, FuncAri = {Func, Ari}, Clauses}, _From, S) ->
- case validate_expect(S#state.mod, Func, Ari, S#state.can_expect) of
+handle_call({expect, FuncAri = {Func, Ari}, Clauses}, _From,
+ S = #state{mod = Mod, expects = Expects}) ->
+ case validate_expect(Mod, Func, Ari, S#state.can_expect) of
ok ->
- NewExpects = store_expect(S#state.mod, FuncAri,
- Clauses,
- S#state.expects),
+ NewExpects = store_expect(Mod, FuncAri, Clauses, Expects),
{reply, ok, S#state{expects = NewExpects}};
{error, Reason} ->
{reply, {error, Reason}, S}
@@ -646,21 +645,44 @@ validate_expect(M, F, A, CanExpect) ->
normal ->
case CanExpect =:= any orelse lists:member({F, A}, CanExpect) of
true -> ok;
- _ -> {error, {undefined_function, {M, F, A}}}
+ _ -> {error, {undefined_function, {M, F, A}}}
end
end.
-init_expects(Mod, Options) ->
- Passthrough = proplists:get_value(passthrough, Options, false),
- case Passthrough andalso exists(Mod) of
- true -> dict:from_list([passthrough_stub(Func, Arity) ||
- {Func, Arity} <- exports(Mod)]);
- _ -> dict:new()
+resolve_can_expect(Exports, Options) ->
+ NonStrict = proplists:get_bool(non_strict, Options),
+ case {Exports, NonStrict} of
+ {_, true} -> any;
+ {undefined, _} -> erlang:exit(undefined_module);
+ _ -> Exports
end.
-passthrough_stub(Func, Arity) ->
+init_expects(Exports, Options) ->
+ Passthrough = proplists:get_bool(passthrough, Options),
+ StubAll = proplists:is_defined(stub_all, Options),
+ Expects = case Exports of
+ undefined ->
+ [];
+ Exports when Passthrough ->
+ [passthrough_stub(FuncArity) || FuncArity <- Exports];
+ Exports when StubAll ->
+ StubRet = case lists:keyfind(stub_all, 1, Options) of
+ {stub_all, RetSpec} -> RetSpec;
+ _ -> val(ok)
+ end,
+ [void_stub(FuncArity, StubRet) || FuncArity <- Exports];
+ Exports ->
+ []
+ end,
+ dict:from_list(Expects).
+
+passthrough_stub({Func, Arity}) ->
{{Func, Arity}, [{arity_2_matcher(Arity), passthrough}]}.
+void_stub({Func, Arity}, RetSpec) ->
+ {{Func, Arity}, [{arity_2_matcher(Arity), RetSpec}]}.
+
+
parse_clause_specs(ClauseSpecs) ->
parse_clause_specs(ClauseSpecs, undefined, []).
@@ -1009,12 +1031,13 @@ get_cover_state(Module, {file, File}) ->
get_cover_state(_Module, _IsCompiled) ->
false.
-exists(Module) ->
- code:which(Module) /= non_existing.
-
-exports(M) ->
- [ FA || FA = {F, A} <- M:module_info(exports),
- normal == expect_type(M, F, A)].
+normal_exports(M) ->
+ try
+ [FA || FA = {F, A} <- M:module_info(exports),
+ normal == expect_type(M, F, A)]
+ catch
+ error:undef -> undefined
+ end.
%% Functions we should not create expects for (auto-generated and BIFs)
expect_type(_, module_info, 0) -> autogenerated;
32 test/meck_tests.erl
View
@@ -825,6 +825,38 @@ passthrough_bif_test() ->
?assertEqual(ok, meck:new(file, [unstick, passthrough])),
?assertEqual(ok, meck:unload(file)).
+stub_all_test() ->
+ ok = meck:new(meck_test_module, [{stub_all, meck:seq([a, b])}]),
+ ok = meck:expect(meck_test_module, a, [], c),
+ ?assertEqual(c, meck_test_module:a()),
+ ?assertEqual(a, meck_test_module:b()),
+ ?assertEqual(b, meck_test_module:b()),
+ ?assertEqual(b, meck_test_module:b()),
+ ?assertEqual(a, meck_test_module:c(1, 2)),
+ ?assertEqual(b, meck_test_module:c(1, 2)),
+ ?assertEqual(b, meck_test_module:c(1, 2)),
+ ok = meck:unload(meck_test_module).
+
+stub_all_default_test() ->
+ ok = meck:new(meck_test_module, [stub_all]),
+ ?assertEqual(ok, meck_test_module:c(1, 2)),
+ ok = meck:unload(meck_test_module).
+
+stub_all_undefined_test() ->
+ ok = meck:new(meck_test_module, [{stub_all, undefined}]),
+ ?assertEqual(undefined, meck_test_module:c(1, 2)),
+ ok = meck:unload(meck_test_module).
+
+stub_all_true_test() ->
+ ok = meck:new(meck_test_module, [{stub_all, true}]),
+ ?assertEqual(true, meck_test_module:c(1, 2)),
+ ok = meck:unload(meck_test_module).
+
+stub_all_overridden_by_passthrough_test() ->
+ ok = meck:new(meck_test_module, [stub_all, passthrough]),
+ ?assertEqual(a, meck_test_module:a()),
+ ok = meck:unload(meck_test_module).
+
cover_test() ->
{ok, _} = cover:compile("../test/meck_test_module.erl"),
a = meck_test_module:a(),
Something went wrong with that request. Please try again.