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.
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.
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}).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With