EUnit 和 io:format

发布于 2024-10-05 09:53:15 字数 705 浏览 2 评论 0原文

我想使用 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,但我实际上想测试输出到控制台(即“结果:5~n”)。

我的这种方法是否错误,因为似乎没有其他人这样做(从我缺乏搜索结果来看)?

背景:第三方代码是一个交互式控制台应用程序,因此所有功能都只是使用 io:format 来显示结果。

I want to test some third-party Erlang code using EUnit.

The output from the code's functions is displayed to the standard output using io:format/2. I would like to capture that output and perform an ?assert test on the string that would be printed out. I cannot modify the third-party code.

Is the a way to do this with Erlang? (For instance, in Java I can simply use System.setOut() to an output stream).

Update:

The group_leader/2 seems to be on the right track.

But, I still don't see how that allows me to capture the string printed by io:format so I can test my assertion. A very simplified example of the code is:

result(Value) ->
    io:format("Result: ~w~n", [Value]).

test_result() ->
    ?assertMatch("Result: 5~n", result(5)).

Clearly, the return from function result/1 is the atom ok, but I actually want to test the string that was output to the console (i.e. "Result: 5~n").

Am I wrong with this approach, because it seems nobody else does this (judging by my lack of search results)?

Background: the third-party code is an interactive console application, so all of the functions just use io:format to show results.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

半衾梦 2024-10-12 09:53:15

方法 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}).

Approach 1: using meck

This code, tested, should do exactly what you are asking for. It does some quite advanced meck tricks (especially when it calls meck:passthrough/0), but I think it is still very clear.

% 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)).

Approach 2: using mock_io

More recently (May 2017) I wrote mock_io, a very simple way to mock both input and output of the Unit Under Test, by implementing the Erlang I/O protocol.

With mock_io, the equivalent code becomes:

% 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}).

Note also that mock_io allows to inject data in the UUT input channel, be it stdin or any other channel. For example:

% 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}).
一瞬间的火花 2024-10-12 09:53:15

看一下 erlang:group_leader/2,使用它你可以设置一个新的组领导者,它将捕获发送的 IO。

我知道 eunit 也这样做是为了捕获测试代码中完成的输出,因此它可能不会很好地发挥作用,您必须尝试一下,看看会发生什么。

Have a look at erlang:group_leader/2, using it you can set a new group leader which will capture the IO which is sent.

I know that eunit does this as well to capture output which is done in the test code so it might not play nice, you'll have to try it out and see what happens.

神魇的王 2024-10-12 09:53:15

Erlang 中的 IO 是通过正常消息传递完成的(除了文件的原始模式之外),因此您可以使用 erlang:group_leader/2 调用将您自己的服务器代替标准 io 服务器。请注意,组领导者是由生成的进程继承的,因此您只需为要从中捕获输出的进程的远前身设置此组领导者即可。然后你可以在你的假 io 服务器中进行一些棘手的过滤或捕获,从而将流量重定向到原始服务器。

对于 io 服务器协议,请参阅 是否有处理 IO 的组领导者协议的规范? 并遵循其中提到的链接。

IO is in Erlang done by normal message passing (with some exception as raw mode of file) so you can put your own server in place of standard io server using erlang:group_leader/2 call. Note that group leader is inherited by spawned processes so you can just set this group leader only for far predecessor of process you would like capture output from. Then you can do some tricky filtering or capturing in your fake io server which cen redirect traffic to original one.

For io server protocol see Is there a specification of the group leader protocol that handles IO? and follow links mentioned there.

梨涡 2024-10-12 09:53:15

您可以使用 dbg(Erlang 跟踪器)来实现此目的。您可以跟踪进程对 io:format/2 的调用并从中接收跟踪消息。您可以使用此跟踪消息来断言用于调用 io:format/2,3 的内容是正确的。这样做的好处是您不必干扰 EUnit,因为它已经在捕获实际的 IO 消息。

一个小例子可以是(根据您的单元测试进行调整):

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

使用它,您还可以测试诸如进程采用正确路径(例如调用您期望的正确函数)或发送正确消息之类的事情其他进程等。它在单元测试中经常被忽视,但它可能非常强大。但有一点是,它可能很快就会变得过度工程化,请小心。

这可能不是公认的答案,但有时它是一个用于测试的好工具:)

祝你好运。

You could use dbg (the Erlang tracer) for this. You can trace the calls made to io:format/2 by a process and receive a trace message from it. You can use this trace message to assert that what is being used to call io:format/2,3 is correct. The benefit from this is that you don't have to interfere with EUnit since it already is capturing the actual IO messages.

A small example could be (adjust to your unit test[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> 

So in other words you simply start the trace before you start your unit test, test the trace messages and then kill the trace. Make sure you only trace on the process making the calls! otherwise you'll get messages from all over the place. You can see how the trace messages look like here: http://www.erlang.org/doc/man/erlang.html#trace-3

Using this you can also test things like that the process takes the correct path (E.g. calls the right functions which you would expect) or sending correct messages to other processes etc. It is often overlooked in unit tests but it can be quite powerful. One point though is that it can quickly become over engineering, be careful.

This might not be the accepted answer but it is a good tool to use for testing sometimes :)

Good luck.

2024-10-12 09:53:15

怎么样: io:format(user,"Result: ~w~n", [Value]) ?

what about: io:format(user,"Result: ~w~n", [Value]) ?

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文