EUnit и io: формат - PullRequest
       34

EUnit и io: формат

7 голосов
/ 02 декабря 2010

Я хочу протестировать сторонний код Erlang с помощью EUnit.

Вывод функций кода отображается на стандартный вывод с помощью 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 для отображения результатов.

Ответы [ 5 ]

4 голосов
/ 19 января 2016

Подход 1: использование 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 г.) я написал mock_io , очень простой способ имитации ввода и вывода тестируемого модуля путем реализации протокола ввода-вывода Erlang.

С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
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}).
3 голосов
/ 02 декабря 2010

IO в Erlang осуществляется обычной передачей сообщений (с некоторым исключением в виде необработанного режима файла), поэтому вы можете поставить свой собственный сервер вместо стандартного сервера io, используя вызов erlang:group_leader/2.Обратите внимание, что лидер группы наследуется порожденными процессами, поэтому вы можете просто установить этого лидера группы только для далекого предшественника процесса, из которого вы хотели бы захватить вывод.Затем вы можете выполнить некоторую хитрую фильтрацию или захват на вашем поддельном сервере ввода-вывода, который перенаправит трафик на исходный.

Протокол сервера ввода-вывода см. Существует ли спецификация протокола лидера группы, который обрабатывает ввод-вывод?1005 * и переходите по ссылкам, указанным там.

3 голосов
/ 02 декабря 2010

Посмотрите на erlang: group_leader / 2, используя его, вы можете установить нового лидера группы, который будет захватывать отправленный IO.

Я знаю, что eunit также делает это для захвата вывода, который делается в тестовом коде, поэтому он может не воспроизводиться нормально, вам придется попробовать его и посмотреть, что произойдет.

2 голосов
/ 03 декабря 2010

Вы могли бы использовать для этого dbg (трассировщик Эрланга).Вы можете отслеживать вызовы, сделанные в io: format / 2 процессом, и получать от него сообщение трассировки.Вы можете использовать это сообщение трассировки, чтобы утверждать, что то, что используется для вызова io: format / 2,3, является правильным.Преимущество этого состоит в том, что вам не нужно вмешиваться в EUnit, поскольку он уже фиксирует фактические сообщения ввода-вывода.

Небольшой пример может быть (приспособиться к вашему тесту [ы]):

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

Используя это, вы также можете тестировать такие вещи, как процесс выбирает правильный путь (например, вызывает нужные функции, которые вы ожидаете) или отправляет правильные сообщения.к другим процессам и т. д. Это часто упускается из виду в модульных тестах, но может быть довольно мощным.Однако один момент заключается в том, что он может быстро превзойти инженерных разработок, будьте осторожны.

Это может быть не принятый ответ, но иногда это хороший инструмент для тестирования:)

Удачи.

0 голосов
/ 03 декабря 2010

как насчет: io: format (пользователь, "Результат: ~ w ~ n", [Значение])?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...