2010-12-02 10 views
7

Quiero probar un código de Erlang de terceros utilizando EUnit.EUnit y io: formato

La salida de las funciones del código se muestra a la salida estándar usando io:format/2. Me gustaría capturar esa salida y realizar una prueba ?assert en la cadena que se imprimirá. No puedo modificar el código de un tercero.

¿Es la forma de hacer esto con Erlang? (Por ejemplo, en Java, simplemente puedo usar System.setOut() a una secuencia de salida).

Actualización:

El group_leader/2 parece estar en el camino correcto.

Pero, todavía no veo cómo me permite capturar la cadena impresa por io:format para poder probar mi afirmación. Un ejemplo muy simplificado del código es:

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

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

Claramente, el regreso de la función result/1 es el átomo ok, pero en realidad me quieren poner a prueba la cadena que había resultado en la consola (es decir "Result: 5~n").

¿Me equivoco con este enfoque, porque parece que nadie más hace esto (a juzgar por mi falta de resultados de búsqueda)?

Antecedentes: el código de un tercero es una aplicación de consola interactiva, por lo que todas las funciones solo usan io:format para mostrar resultados.

Respuesta

2

Usted podría utilice dbg (el trazador de Erlang) para esto. Puede rastrear las llamadas realizadas a io: format/2 por un proceso y recibir un mensaje de seguimiento de la misma. Puede usar este mensaje de seguimiento para afirmar que lo que se está utilizando para llamar a io: format/2,3 es correcto. El beneficio de esto es que no tiene que interferir con EUnit ya que está capturando los mensajes IO reales.

Un pequeño ejemplo podría ser (a adaptarse a su unidad de prueba [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,[email protected],1}]} 
6> dbg:tp(io, format, []). 
{ok,[{matched,[email protected],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> 

Así, en otras palabras, sólo tiene que iniciar el rastreo antes de empezar su unidad de prueba, prueba de los mensajes de seguimiento y luego matar el rastro¡Asegúrese de rastrear solo el proceso de realizar las llamadas! de lo contrario, recibirá mensajes de todas partes. Puede ver cómo se ven los mensajes de seguimiento aquí: http://www.erlang.org/doc/man/erlang.html#trace-3

Usando esto también puede probar cosas como que el proceso toma la ruta correcta (por ejemplo, llama a las funciones correctas que podría esperar) o enviar mensajes correctos a otros procesos, etc. . A menudo se pasa por alto en las pruebas unitarias, pero puede ser bastante poderoso. Sin embargo, un punto es que puede rápidamente convertirse en exceso de ingeniería, tenga cuidado.

Esto podría no ser la respuesta aceptada, pero es una buena herramienta a utilizar para las pruebas de veces :)

Buena suerte.

+0

Una respuesta muy detallada, necesitaré un poco de tiempo para entender su sugerencia. Gracias. – Max

3

Eche un vistazo a erlang: group_leader/2, al usarlo puede establecer un nuevo líder de grupo que capturará el IO que se envía.

Sé que eunit hace esto también para capturar la salida que se hace en el código de prueba para que no se reproduzca bien, tendrás que probarlo y ver qué pasa.

+0

Voy a echar un vistazo, gracias Lukas. – Max

3

IO está en Erlang hecho mediante el envío de mensajes normales (con algunas excepciones como modo de archivo sin formato) para que pueda poner su propio servidor en lugar del servidor io estándar usando la llamada erlang:group_leader/2. Tenga en cuenta que el líder de grupo es heredado por procesos engendrados, por lo que puede establecer este líder de grupo solo para el predecesor más lejano del proceso del que desea obtener la salida de captura. Luego puede hacer algunos filtros o capturas difíciles en su servidor falso que redirecciona el tráfico al original.

Para el protocolo del servidor io vea Is there a specification of the group leader protocol that handles IO? y siga los enlaces mencionados allí.

+0

Gracias también Hynek. Voy a leer un poco. – Max

0

¿qué ocurre con: io: formato (usuario, "Resultado: ~ w ~ n", [Valor])?

+0

Como expliqué, las llamadas a io: format() están en código de terceros que no puedo modificar. Por lo tanto, no veo cómo su sugerencia ayuda en mi caso particular. – Max

+0

bien, me perdí esa parte, lo siento. Sin embargo, es un sistema muy extraño en ese caso. La salida de la consola siempre se consideró como un efecto secundario, que no forma parte de la funcionalidad. En ese caso, sobrecargaría/simularía el módulo io y capturaría la salida de la consola. – user425720

4

Enfoque 1: utilizando meck

Este código, prueba, debe hacer exactamente lo que está pidiendo. Hace algunos trucos de Meck bastante avanzados (especialmente cuando llama al meck:passthrough/0), pero creo que todavía está muy claro.

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

Enfoque 2: utilizando mock_io

Más recientemente (mayo de 2017) escribí mock_io, de una manera muy sencilla para burlarse tanto de entrada como de salida de la unidad bajo prueba, mediante la implementación del Erlang I/O protocolo.

Con mock_io, el código equivalente se convierte en:

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

Nótese también que mock_io permite inyectar datos en el canal de entrada del TUS, ya sea la entrada estándar o cualquier otro canal. Por ejemplo:

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