【问题标题】:EUnit and io:formatEUnit 和 io:format
【发布时间】:2017-05-10 13:56:55
【问题描述】:

我想使用 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来显示结果。

【问题讨论】:

    标签: erlang


    【解决方案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 输入通道中注入数据,无论是标准输入还是任何其他通道。例如:

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

    【讨论】:

      【解决方案2】:

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

      我知道 eunit 也这样做是为了捕获在测试代码中完成的输出,所以它可能不会很好玩,你必须尝试一下,看看会发生什么。

      【讨论】:

        【解决方案3】:

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

        对于 io 服务器协议,请参阅 Is there a specification of the group leader protocol that handles IO? 并点击那里提到的链接。

        【讨论】:

          【解决方案4】:

          可以为此使用 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

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

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

          祝你好运。

          【讨论】:

          • 非常详细的回复,我需要一点时间来了解您的建议。谢谢。
          【解决方案5】:

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

          【讨论】:

          • 正如我所解释的,io:format() 调用位于我无法修改的第三方代码中。所以,我看不出你的建议对我的特殊情况有什么帮助。
          • 好的,我错过了那部分,抱歉。然而,在这种情况下,这是一个非常奇怪的系统。控制台输出始终被视为副作用,它不是功能的一部分。在这种情况下,我会重载/模拟 io 模块并捕获控制台输出。
          猜你喜欢
          • 2012-05-26
          • 1970-01-01
          • 2012-04-06
          • 2011-09-23
          • 2013-01-03
          • 2013-08-04
          • 2012-02-28
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多