aprendiendo ( Erlang ).

miércoles, 18 de julio de 2012

EUnit (I). Pruebas unitarias.

| 0 comentarios |

El software adolece de un mal endémico, y es que, salvo excepciones, es complejo. Es por ello, que se hace necesario un sistema de pruebas que te permita verificar que el código realizado es correcto. En un nivel más básico, están las pruebas unitarias y es donde entra en juego la librería EUnit de Erlang.

Las pruebas unitarias son las que se realizan para verificar, de forma independiente, que un bloque de código, función y/o módulo funcionan correctamente.

La unidad básica de las pruebas unitarias es el caso de prueba. El caso de prueba es, en nuestro caso, una función, o conjunto de ellas, que comprueba la corrección de una funcionalidad concreta. Al conjunto de casos de pruebas se le suele llamar suite de pruebas.

Ventajas.

La principal ventaja de un sistema de pruebas unitarias es evitar o detectar la introducción de fallos en nuestra aplicación y/o librería. Supón que tienes una librería que pasa todas las pruebas, y que sabemos por experiencia que funciona bien. Pues llega un buen día que, decides mejorar el código y lo cambias. Lo normal es que, en algún momento, podamos introducir algún bug en nuestro código sin darnos cuenta. Es ahora donde es realmente útil un sistema de pruebas, ya que al intentar pasar la pruebas detectaremos el error introducido en el código.

Otro punto a su favor es que nos proporciona la respuesta a dos grandes preguntas ¿en qué falla? y ¿dónde falla?. Nuestro caso de prueba, en el momento de fallo, nos proporciona las respuestas a dichas preguntas. Preguntas que se me antojan muy importantes cuando el sistema esta en producción y no se le ha integrado ningún sistema de pruebas.

A todos nos pasa, que cuando realizamos un código, pasado un tiempo, se nos olvida realmente cual era su cometido. Ya sea por que no lo documentamos correctamente o bien, por que en aquel momento era obvia su funcionalidad pero … ahora no. Si en su momento nos preocupamos de integrarlo con un sistema de pruebas, este, nos puede servir de documentación y así saber, que realiza ese dichoso código que ahora no vemos.

Otra gran ventaja. Documentar los fallos. Si, ahora que tenemos nuestro sistema en producción sabes que funciona correctamente pero llega un buen día que … un bug!. Entonces lo corriges y … ¿otro bug? Y lo corriges y … otra vez el primer bug?. Esto no pasaría si en el momento de detectar el primer bug le hubiéramos realizado un caso de prueba que detectará dicho bug. Así de esta forma, no sólo corregimos y documentamos el fallo, sino que además, evitamos que vuelva a suceder.

Inconvenientes.

El principal inconveniente es que hay que realizar una gran cantidad de código extra. Trabajo que a veces no puedes, o no te apetece, realizar y que a la larga acabarás necesitando. Si, acabarás suplicando … cuando los fallos, aparezcan y reaparezcan … cuando donde debía haber … haya … cuando toques aquí y se rompa allá. No nos llevemos a engaño, el software que no se prueba como es debido con el tiempo se deteriora … se vuelve frágil … y en muchos casos inmanejable.

Las ventajas explicadas no son gratis y requiere de un esfuerzo inicial y continuado.

Otro inconveniente es que no es infalible. Como vimos los fallos se seguirán produciendo. No olvidemos que las pruebas las realizamos nosotros con estas manitas.

Las pruebas unitarias no son la panacea. No detectan errores de rendimiento o de integración por poner algunos ejemplos. Lo suyo es que acaben formando parte de un sistemas de pruebas que nos permita verificar otros aspectos del software. Pero en todo caso, debe o debería ser una parte integrante de nuestro que hacer diario.

Convenciones y alguna que otra cosa.

Lo primero que hay que tener claro es … ¿Dónde vamos a realizar los casos de prueba? Se pueden realizar en el mismo módulo que estamos implementando, pero para mi gusto emborronan el código, y prefiero implementarlos en un módulo aparte.

Si como yo, prefieres poner las pruebas o casos de prueba en un módulo aparte, este módulo tendrá el mismo nombre que el módulo original terminado en _tests. Por ejemplo, mi módulo prueba tendrá un módulo para las pruebas llamado prueba_tests.

Otra convención, son los nombres de los casos de prueba. Estos tienen que acabar en _test.

Con estas convenciones, cuando le digamos a EUnit que queremos probar el módulo prueba el sabrá automáticamente que puede tener un módulo llamado prueba_tests y que los casos de prueba son todas aquellas funciones que terminen en _test.

Mis primeras pruebas.

Una vez que tenemos claro algunas convenciones vamos a pasar al código, que es lo nuestro. Lo primero, implementar el módulo a probar. Los muy ortodoxos primero implementan las pruebas, pero para gustos colores.

-module(prueba).
-export([sumar/2, restar/2, multiplicar/2, dividir/2]).

sumar (A, B) ->
    A + B.

restar (A, B) ->
    A - B.

multiplicar (A, B) ->
    A * B.

dividir ( A, B ) ->
    A div B.

Ahora, el módulo de pruebas con los casos de prueba. Lo primero que tenemos que hacer es incluir el fichero cabecera del EUnit. Veamos como:

-module(prueba_tests).
-include_lib("eunit/include/eunit.hrl").

sumar_test () ->
    0 = prueba:sumar(0,0),
    1 = prueba:sumar(1,0),
    4 = prueba:sumar(2,2),
    5 = prueba:sumar(3,2).

restar_test () ->
    0 = prueba:restar(0,0),
    -1 = prueba:restar(0,1),
    1 = prueba:restar(4,3),
    0 = prueba:restar(4,4).

multiplicar_test () ->
    0 = prueba:multiplicar(0, 1),
    0 = prueba:multiplicar(1, 0),
    -2 = prueba:multiplicar(2, -1),
    4 = prueba:multiplicar(2, 2),
    8 = prueba:multiplicar(4, 2).

dividir_test () ->
    0 = prueba:dividir(0, 2),
    1 = prueba:dividir(8, 8),
    2 = prueba:dividir(8, 4),
    -2 = prueba:dividir (-8, 4).

Se trata de un módulo muy simple y por lo tanto, el módulo de pruebas también. Como observarás hemos implementado un caso de prueba por cada función implementada. Lo normal es que exista más de un caso de prueba por función implementada, en el módulo original. También es destacable que no es necesario exportar las funciones que implementemos.

1> c(prueba).
{ok,prueba}
2> c(prueba_tests).
{ok,prueba_tests}
3> eunit:test(prueba).
  All 4 tests passed.
ok

Compilamos y ejecutamos las pruebas de nuestro módulo prueba con una llamada a la función eunit:test/1. Fácil verdad. Pero esta mal. Si, si. Esta mal. Simplemente nos hemos limitado a hacer matching, que pasa si pretendo probar la división por cero ¿Cómo lo hacemos? ¿Cómo comprobamos excepciones? Para resolver este problemas tenemos las afirmaciones o las assertions.

Assertions.

Como medida intuitiva, para realizar pruebas sobre funciones que devuelvan excepciones, sería envolverlas en una sentencia try. Pero imagínate, lo largo que se puede volver, y sobre todo lo que emborronaría el código. Por este motivo, Eunit, nos proporciona un conjunto de macros que pretenden ayudarnos. Estas macros son assertions o afirmaciones.

  • ?assert(Expresión), ?assertNot(Expresión): Se utilizar para validar expresiones booleanas. Es decir, que nos verifica si una expresión dada es cierta o no.
  • ?assertEqual(A, B), ?assertNotEqual(A, B): Afirmación para igualdad (estricta) de A y B. Es decir, si es cierta la igualdad A=:=B.
  • ?assertMatch(Patrón, Expresión), ?assertNotMatch(Patrón, Expresión): Esta macro nos permite realizar afirmaciones de matching. Es decir, validar expresiones del tipo Patrón = Expresión.
  • ?assertError(Patrón, Expresión): Afirmación para la comprobación de errores. Donde el patrón se refiere al tipo de error.
  • ?assertThrow(Patrón, Expresión): No permite comprobar errores elevados con throw(Patrón).
  • ?assertExit(Patrón, Expresión): Comprueba salidas que fueron lanzadas con la función exit(Patrón).
  • ?assertException(Clase, Patrón, Expresión), assertNotException(Clase, Patrón, Expresión): Se trata de una versión genérica de las tres anteriores, donde clase puede ser error , throw o exit.

Ahora ya estamos en disposición de hacer los casos de prueba en condiciones.

Reimplementando los casos de prueba.

Veamos como quedarían nuestras pruebas utilizando los asserts.

-module(prueba_tests).
-include_lib("eunit/include/eunit.hrl").

sumar_test () ->
    ?assertEqual(0, prueba:sumar(0,0)),
    ?assertEqual(1, prueba:sumar(1,0)),
    ?assertMatch(4, prueba:sumar(2,2)),
    ?assert(is_number(prueba:sumar(3,2))).

restar_test () ->
    ?assertEqual(0, prueba:restar(0,0)),
    ?assertEqual(-1, prueba:restar(0,1)),
    ?assertEqual(1, prueba:restar(4,3)),
    ?assertEqual(0, prueba:restar(2,2)).

multiplicar_test () ->
    ?assertEqual(0, prueba:multiplicar(0, 1)),
    ?assertEqual(0, prueba:multiplicar(1, 0)),
    ?assertEqual(-2, prueba:multiplicar(2, -1)),
    ?assertEqual(4, prueba:multiplicar(2, 2)),
    ?assertEqual(8, prueba:multiplicar(4, 2)).

dividir_test () ->
    ?assertEqual(0, prueba:dividir(0, 2)),
    ?assertEqual(1, prueba:dividir(8, 8)),
    ?assertEqual(2, prueba:dividir(8, 4)),
    ?assertEqual(-2, prueba:dividir (-8, 4)),
    ?assertException(error, badarith, prueba:dividir(1, 0)),
    ?assertError(badarith, prueba:dividir(1, 0)).

He intentado diversificar uso de las afirmaciones para que el ejemplo sea más amplio. También he incluido el control de división por cero en el dividir_test/0.

1> c(prueba_tests).
{ok,prueba_tests}
2> eunit:test(prueba).
  All 4 tests passed.
ok

Fallo en las pruebas.

Hasta el momento no hemos visto como fallan las pruebas y que información nos proporciona EUnit. Vamos a crear un test para que falle y así ilustrar este comportamiento.

fallo_test () ->
    ?assertEqual(0, 3).

Esta claro que la afirmación que hemos realizado en el este caso de pruebas no es cierta. Es decir, que no podemos afirmar la igualdad de 0 y 3.

1> c(prueba_tests).
{ok,prueba_tests}
2> eunit:test(prueba).
prueba_tests: fallo_test...*failed*
::error:{assertEqual_failed,[{module,prueba_tests},
                           {line,39},
                           {expression,"3"},
                           {expected,0},
                           {value,3}]}
  in function prueba_tests:'-fallo_test/0-fun-0-'/1


=======================================================
  Failed: 1.  Skipped: 0.  Passed: 4.
error

La gran ventaja de los asserts. Es precisamente cuando falla el test. Ya que el assert nos proporciona una información fundamental para saber ¿dónde falla? y ¿qué falla?.

El fallo del caso de prueba nos esta indicando que ha fallado en el módulo prueba_tests en la línea 39. También nos indica que tenemos una expresión que evalúa al valor 3 y se esperaba un valor 0.

Como diría un amigo, Ahora … probad malditos … prodad…

Publicar un comentario en la entrada

0 comentarios:

 
Licencia Creative Commons
Aprendiendo Erlang por Verdi se encuentra bajo una Licencia Creative Commons Atribución-NoComercial-CompartirIgual 3.0 Unported.