Posted on Thursday, 9th October 2008 by charpi

In my previous post, I described a way to mock module by replacing
them. Today, I’ll post a version closer to other mock
library. Basically, I create on mock module on the fly and forward all
calls to a process. You can tell to this process how to answer for
each calls.
Here is the test :

%%% Copyright (c) 2008 Nicolas Charpentier
%%% All rights reserved.
-module(mock_test).

-export([test /0]).

test () ->
    lazy_way () ,
    dynamic_way (),
    ok.

lazy_way () ->
    "i'm the production code" = my_module: who(),
    mock: replace_module (my_module, my_mock_module),
    "I'm the mocked code" = my_module: who (),
    mock: uninstall (my_module),
    "i'm the production code" = my_module: who (),
    ok.

dynamic_way () ->
    "i'm the production code" = my_module: who (),
    {pong,host} = my_module: ping (host),

    mock: start (),
    mock: add_module(my_module),
    mock: set_answer (my_module, who, "I'm the mocked code"),
    "I'm the mocked code" = my_module: who (),
    mock: set_answer (my_module, who, "Oops did it wrong"),
    "Oops did it wrong" = my_module: who (),

    [{my_module, who, []}, {my_module, who, []}] =  mock: calls (),
    [] =  mock: calls (),

    case catch my_module: ping(host) of
        error_no_response -> ok;
        Other -> exit({unexepected_mock_response, Other})
    end,

    mock: set_answer (my_module, ping, {pang, mock_host}),
    {pang, mock_host} = my_module: ping(last_host),

    [{my_module, ping, [host]},
     {my_module, ping, [last_host]}] =  mock: calls (),

    mock: uninstall (my_module),
    "i'm the production code" = my_module: who (),
    {pong,host} = my_module: ping (host),
    ok.

and the implementation

%%% Copyright (c) 2008 Nicolas Charpentier
%%% All rights reserved.
-module(mock).

-export([replace_module /2]).
-export([uninstall /1]).

-export([start /0]).
-export([add_module /1]).
-export([set_answer /3]).
-export([calls /0]).

replace_module (Module, Mock_module) ->
    uninstall (Module),
    {ok, Binary} = file: read_file (code: which (Mock_module)),
    File_name = atom_to_list (Module) ++ ".erl",
    code: load_binary(Module, File_name, Binary),
    ok.

uninstall (Module) ->
    code: purge (Module),
    code: delete (Module).

start () ->
    Pid = spawn_link (fun () -> mocker ([],[]) end),
    register(mocker, Pid),
    ok.

add_module (Module) ->
    Forms = forms (Module),
    uninstall (Module),
    {ok, _, Binary} = compile: forms(Forms, [report]),
    code: load_binary (Module, "foo.erl", Binary),
    ok.

set_answer (Module, Function, Answer) ->
    mocker ! {set_answer, Module, Function, Answer},
    ok.

calls () ->
    mocker ! {self(), calls},
    receive
        {calls, Calls} ->
            Calls
    end.

forms (Module) ->
    Exported_functions = find_exported_functions (Module),
    Fun = fun (F) -> function_to_form (Module, F) end,
    Functions_forms = [Fun(F) || F <- Exported_functions],
    [{attribute,1,module,Module},
     {attribute,3,export,Exported_functions}] ++
        Functions_forms  ++
        [ {function,16,wait_response,0,
           [{clause,16,[],[],
             [{'receive',20,
               [{clause,20,
                 [{tuple,20,[{atom,20,response},{atom, 20, undefined}]}],
                 [],
                 [{call,20,
                   {atom,20,throw},
                   [{atom,20,error_no_response}]}]},
                {clause,20,
                 [{tuple,20,[{atom,20,response},{var,20,'Response'}]}],
                 [],
                 [{var,19,'Response'}]}
                ]}]}]},
          {eof,23}].

function_to_form (Module, {Function, Arity}) ->
    Parameters = parameters (Arity),
    Parameters_cons = parameters_cons (Arity),
    {function,5,Function,Arity,
     [{clause,5,Parameters,[],
       [{op,6,'!',
         {atom,6,mocker},
         {tuple,6,
          [{call,6,{atom,6,self},[]},
           {atom,6,forward},
           {atom,6,Module},
           {atom,6,Function},
           Parameters_cons]}},
        {call,8,{atom,8,wait_response},[]}
       ]}]}.

parameters (0) ->
    [];
parameters (N) ->
    Seq = lists: seq (1,N),
    F = fun (I) ->
                String = lists:flatten (io_lib: format ("Var~p",[I])),
                list_to_atom(String)
        end,
    [{var, 6, F(I)} || I <- Seq].

parameters_cons (N) ->
    parameter_list_form (parameters (N)).

put_parameter_in_call (Parameters) ->
    list_to_tuple (transform_list_to_cons (Parameters)).

transform_list_to_cons ([]) ->
    [nil, 6];
transform_list_to_cons ([H|T]) ->
    [cons, 6, H, put_parameter_in_call (T)].

parameter_list_form ([]) ->
    {nil,6};
parameter_list_form (Variable_forms) ->
    put_parameter_in_call (Variable_forms).

find_exported_functions (Module) ->
    Module_info = Module: module_info (),
    All_exported = proplists: get_value (exports, Module_info),
    lists: filter (fun ({module_info,_}) ->
                           false;
                       (_) ->
                           true
                   end, All_exported).

mocker (Modules, Calls) ->
    receive
        {From, calls} ->
            From ! {calls,lists: reverse (Calls)},
            mocker (Modules, []);
        {set_answer, Module, Function, Answer} ->
            New_modules = proplists: delete ({Module, Function}, Modules),
            mocker ([{{Module, Function}, Answer}|New_modules], Calls);
        {From, forward, Module, Function, Args} ->
            Response = proplists: get_value ({Module,Function}, Modules),
            From ! {response, Response},
            mocker (Modules, [{Module, Function, Args}|Calls])
    end.

Tags: ,
Posted in Uncategorized | Comments (4)

4 Responses to “Enhanced erlang mock implementation”

  1. Matthew Says:

    Awesome! I was looking around for a mock framework for Erlang and this fits the bill very nice indeed. Any plans packaging this up and releasing it somewhere?

  2. charpi Says:

    Hi Matthew,
    I’ll pack it in the next days on my trac site.
    Just a remark, for me we don’t need any mock framework for erlang as erlang offers a lot of functionality to easily write unit tests.
    The only thing that let me think about a mock framework is the legacy code.
    Can you tell me why you need one ?

  3. Matthew Says:

    My erlang experience is not significant maybe I am doing something wrong - you tell me. But when I code a function, it often times calls other modules, sends messages (via calls to a module’s client api), etc… I like your mock framework because it allows me test my function in isolation. Should I be coding differently? Maybe by passing in a high-order fun to call? The problem I have with that is it results in a lot of code like this:

    fun1 (MyArg) ->
    fun1(MyArg, fun amodule:modulefun/1).

    fun1(MyArg, HighOrderFun) ->
    %% do stuff
    HighOrderFun(MyArg).

    fun1_test () ->
    fun1(TheArg, fun(AnArg) -> %do stuff end).

    Which just seems like a lot of wrapping… What do you think?

  4. madlep Says:

    This is excellent. Just what I’ve been looking for. Coming from a Java/Ruby TDD background, mocking and test first development feels the natural way to go now. Erlang rocks, but it doesn’t seem like Agile/TDD has as big a focus in the Erlang community.

    It just feels a little weird coding without the peace of mind good tests give you.

    A slightly related question I’ve had, is what is the best way to test code that uses multiple processes in Erlang? Testing sequential code is straight forward with eunit, but when it comes to testing code that interacts with other processes, there doesn’t seem to be much guidance on how to approach it.

Leave a Reply