我想使用EUnit测试一些第三方Erlang代码。
代码功能的输出使用io:format/2
显示在标准输出中。我想捕获该输出并对要打印的字符串执行?assert
测试。我无法修改第三方代码。
这是用Erlang做的一种方法吗? (例如,在Java中,我可以简单地将System.setOut()用于输出流。)
更新:
group_leader/2
似乎走在了正确的轨道上。
但是,我仍然没有看到它如何允许我捕获由io:format
打印的字符串,因此我可以测试我的断言。一个非常简单的代码示例是:
result(Value) ->
io:format("Result: ~w~n", [Value]).
test_result() ->
?assertMatch("Result: 5~n", result(5)).
显然,函数result/1
的返回是原子ok
,但实际上我想测试输出到控制台的字符串(即"Result: 5~n"
)。
这种方法我错了,因为似乎没有其他人这样做(根据我缺乏搜索结果判断)?
背景:第三方代码是一个交互式控制台应用程序,因此所有功能都只使用io:format
来显示结果。
答案 0 :(得分:4)
方法1:使用meck
此代码经过测试,应该完全符合您的要求。它做了一些非常先进的meck技巧(特别是当它调用meck:passthrough/0
时),但我认为它仍然很清楚。
% UUT
foo() ->
io:format("Look ma no newlines"),
io:format("more ~w~n", [difficult]),
io:format("~p dudes enter a bar~n", [3]),
ok.
% Helper: return true if mock Mod:Fun returned Result at least once.
meck_returned(Mod, Fun, Result) ->
meck_returned2(Mod, Fun, Result, meck:history(Mod)).
meck_returned2(_Mod, _Fun, _Result, _History = []) ->
false;
meck_returned2(Mod, Fun, Result, _History = [H|T]) ->
case H of
{_CallerPid, {Mod, Fun, _Args}, MaybeResult} ->
case lists:flatten(MaybeResult) of
Result -> true;
_ -> meck_returned2(Mod, Fun, Result, T)
end;
_ -> meck_returned2(Mod, Fun, Result, T)
end.
simple_test() ->
% Two concepts to understand:
% 1. we cannot mock io, we have to mock io_lib
% 2. in the expect, we use passthrough/0 to actually get the output
% we will be looking for in the history! :-)
ok = meck:new(io_lib, [unstick, passthrough]),
meck:expect(io_lib, format, 2, meck:passthrough()),
?assertMatch(ok, foo()),
%?debugFmt("history: ~p", [meck:history(io_lib)]),
?assert(meck_returned(io_lib, format, "Look ma no newlines")),
?assert(meck_returned(io_lib, format, "more difficult\n")),
?assert(meck_returned(io_lib, format, "3 dudes enter a bar\n")),
?assertNot(meck_returned(io_lib, format, "I didn't say this!")),
?assert(meck:validate(io_lib)).
方法2:使用mock_io
最近(2017年5月)我写了mock_io,这是一种通过实现Erlang I / O协议来模拟被测单元的输入和输出的一种非常简单的方法。
使用mock_io,等效代码变为:
% UUT
foo() ->
io:format("Look ma no newlines"),
io:format("more ~w~n", [difficult]),
io:format("~p dudes enter a bar~n", [3]),
ok.
simple_test() ->
Expected = <<"Look ma no newlines"
"more difficult\n",
"3 dudes enter a bar\n">>,
{Pid, GL} = mock_io:setup(),
?assertMatch(ok, foo()),
?assertEqual(Expected, mock_io:extract(Pid)),
mock_io:teardown({Pid, GL}).
另请注意,mock_io允许在UUT输入通道中注入数据,无论是stdin还是任何其他通道。例如:
% UUT
read_from_stdin() ->
io:get_line("prompt").
% Test
inject_to_stdin_test() ->
{IO, GL} = mock_io:setup(),
mock_io:inject(IO, <<"pizza pazza puzza\n">>),
?assertEqual("pizza pazza puzza\n", uut:read_from_stdin()),
?assertEqual(<<>>, mock_io:remaining_input(IO)),
mock_io:teardown({IO, GL}).
答案 1 :(得分:3)
看看erlang:group_leader / 2,使用它你可以设置一个新的组长,它将捕获发送的IO。
我知道eunit也可以捕获在测试代码中完成的输出,因此它可能不会很好用,你必须尝试一下,看看会发生什么。
答案 2 :(得分:3)
IO在Erlang中通过正常的消息传递完成(有一些例外作为文件的原始模式),因此您可以使用erlang:group_leader/2
调用将您自己的服务器替换为标准的io服务器。请注意,组生成器由生成的进程继承,因此您只能为要从中捕获输出的进程的前一个进程设置此组长。然后你可以在假的io服务器上进行一些棘手的过滤或捕获,这会将流量重定向到原始的。
对于io服务器协议,请参阅Is there a specification of the group leader protocol that handles IO?并按照其中提到的链接进行操作。
答案 3 :(得分:2)
你可以使用dbg(Erlang跟踪器)。您可以跟踪进程对io:format / 2的调用,并从中接收跟踪消息。您可以使用此跟踪消息断言用于调用io:format / 2,3的内容是正确的。这样做的好处是您不必干扰EUnit,因为它已经捕获了实际的IO消息。
一个小例子可以(适应你的单元测试[s]):
1> HandleFun = fun(Trace, Parent) -> Parent ! Trace, Parent end.
#Fun<erl_eval.12.113037538>
2> dbg:tracer(process, {HandleFun, self()}).
{ok,<0.119.0>}
3> IOCallingFun = fun(F) ->
3> timer:sleep(5000),
3> io:format("Random: ~p~n",[random:uniform(1000)]),
3> F(F)
3> end.
#Fun<erl_eval.6.13229925>
4> PidToTrace = erlang:spawn_link(fun() -> IOCallingFun(IOCallingFun) end).
<0.123.0>
Random: 93
Random: 444
5> dbg:p(PidToTrace, [c]).
{ok,[{matched,nonode@nohost,1}]}
6> dbg:tp(io, format, []).
{ok,[{matched,nonode@nohost,3}]}
Random: 724
Random: 946
Random: 502
7> flush().
Shell got {trace,<0.123.0>,call,{io,format,["Random: ~p~n",[724]]}}
Shell got {trace,<0.123.0>,call,{io,format,["Random: ~p~n",[946]]}}
Shell got {trace,<0.123.0>,call,{io,format,["Random: ~p~n",[502]]}}
ok
8> exit(PidToTrace).
** exception exit: <0.123.0>
9> dbg:stop_clear().
ok
10>
因此换句话说,您只需在开始单元测试之前启动跟踪,测试跟踪消息然后终止跟踪。 请确保您只跟踪拨打电话的流程!否则您将收到来自各地的消息。您可以在此处查看跟踪消息的外观:http://www.erlang.org/doc/man/erlang.html#trace-3
使用它你也可以测试过程采用正确的路径(例如调用你期望的正确函数)或向其他进程发送正确的消息等。在单元测试中经常被忽略但它可能是相当的强大。但有一点是它可以快速变得过度工程,小心。
这可能不是公认的答案,但它有时候用于测试是一个很好的工具:)
祝你好运。答案 4 :(得分:0)
怎么样:io:格式(用户,“结果:~w~n”,[Value])?