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: erlang, mock object
Posted in Uncategorized | Comments (7)

October 10th, 2008 at 4:50 am
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?
October 10th, 2008 at 5:25 pm
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 ?
October 15th, 2008 at 9:01 am
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?
December 28th, 2008 at 7:16 am
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.
February 26th, 2009 at 4:16 pm
Hi,
Check out my approach which is closely modeled after easymock:
http://sheyll.blogspot.com/2009/02/erlang-mock-erlymock.html
March 8th, 2010 at 1:25 pm
Don’t you need a command to stop the mocker process? When I run my test suite a second time, if fails on registering the mocker process.
September 7th, 2010 at 2:28 am
If you are looking for complete mock library, have a look to
http://github.com/esl/meck