Test Unitarios; Framework DUnit (Entrega 2)
Esta es la segunda entrada de la serie dedicada a pruebas unitarias. Si en la primera vimos una introducción a la programación guiada por pruebas, en esta segunda vamos a empezar a revisar los frameworks disponibles que nos ayudan a realizar los test unitarios.
Concretamente en esta nos centraremos en DUnit.
- Test Unitarios; Introducción (Entrega 1)
- Test Unitarios; Framework DUnit (Entrega 2)
- Convertir Test de DUnit a DUnitX (Entrega 3)
- Test Unitarios; Framework DUnitX (Entrega 4)
- Entornos de Ejecución (“Bonus track”)
Para realizar Test unitarios con Delphi disponemos de dos frameworks; Según la versión de Delphi podemos utilizarlos indistintamente, aunque a partir de la versión XE8 de Delphi, se recomienda utilizar DUnitX.
Contenido
CREACIÓN DE PRUEBAS UNITARIAS (FRAMEWORKS)
Para realizar test unitarios con Delphi podemos utilizar los frameworks (ambos Open Source) DUnit y DUnitX.
A partir de la versión Delphi XE8 Embarcadero recomienda utilizar DUnitX, ya que DUnit ha quedado desactualizado. Si se ha trabajado con DUnit es relativamente fácil migrar los test ya existentes a DUnitX.
Lo que vamos a hacer es realizar los test con ambos paquetes y de esta forma también podremos ver las diferencias entre ambos y las ventajas prácticas que nos pueden aportar.
FRAMEWORK DUNIT
Para crear nuestro proyecto de pruebas unitarias que testee la clase TAritmeticaBasica de nuestra unit UClass.TAritmeticaBasica, podemos utilizar el asistente que ya trae el paquete. Si en nuestra versión de delphi no está instalado este paquete (DUnit), hay que descargarlo (de la dirección que hay más arriba) e instalarlo.
- Lo primero es crear un proyecto que utilice nuestra Unit. Sería nuestra aplicación que va a utilizar esta unit. Para ello crearemos un nuevo proyecto llamado PAritmeticaBasica y le añadiremos la unit anterior.
- A continuación vamos a generar, utilizando el asistente, el proyecto para las pruebas unitarias (y lo añadiremos al mismo grupo de proyectos). De esta forma tendremos nuestro proyecto y junto a el el proyecto de pruebas.
Para ello, desde el menú de File/New/Other/Unit Test seleccionamos Test Project.
1.- Arranca el asistente y lo configuramos de la siguiente forma:
- Es el proyecto que usa nuestra Unit
- Es el proyecto nuevo de testing que se va a generar.
2.- El segundo paso del asistente nos pregunta el framework de test a utilizar y el tipo de ejecución. Para este caso seleccionaremos lo siguiente:
3.- Una vez finalizamos el asistente, ya disponemos en nuestro grupo de los 2 proyectos que hemos creado. El de nuestra aplicación y el de los test unitarios asociados. No hemos acabado aquí, pues el Framework nos ofrece la posibilidad de crear además la estructura de los test que necesitamos.
4.- Seleccionando el proyecto de test «PAritmeticaBasicaDUnitTest«, desde el menú:
File/New/Other/Unit Test seleccionamos Test Case. Arrana el segundo Wizard, en este caso no para el proyecto, sino para los test.
5.- Seleccionamos la Unit sobre la que queremos crear los test y automáticamente el asistente nos mostrará los métodos disponibles para que podamos seleccionar cuales queremos probar ( a cuales queremos generarle pruebas unitarias).
6.- Vemos que en esa lista ya aparecen los métodos de nuestra clase TAritmeticaBasica. Seleccionamos todos y pulsamos siguiente.
7.- El último paso define el nombre para la unit y el proyecto al que la vamos a añadir.
8.- Una vez finalizado el asistente, guardamos los cambios y ya disponemos de los 2 proyectos en un grupo. El primero que usa nuestra unit y el segundo que testea dicha unit. Por ahora todo está en el «esqueleto» y sin implementación, pero ya podemos compilar y ejecutar nuestro proyecto de test (ver NOTA1).
NOTA1: Para compilar y ejecutar debemos modificar el procedimiento TestFactorizacion, de la unit Test.UClass.TAritmeticaBasica.pas y dejarlo como el que hay a continuación.
procedure TestTAritmeticaBasica.TestFactorizacion; var ALista: System.Generics.Collections.TList; ANumero: Int64; begin // TODO: Setup method call parameters {FAritmeticaBasica.Factorizacion(ANumero, ALista);} // TODO: Validate method results end; |
Comentamos la línea porque el elemento en nuestro proyecto de ejemplo llega sin asignar y completamos la definición (que el asistente no acaba de completarla bien).
Si ejecutamos nuestro test, ya podemos ver los resultados.
Como podemos ver los test (con lo que actualmente tenemos) dan todos como correctos. Es normal, porque ni tenemos código en nuestra función, ni tenemos validaciones correctas en nuestros test.
Si seguimos los pasos que definimos en la primera entrega de esta serie para la definición y creación de test unitarios, lo siguiente que debemos hacer es dotar de lógica a nuestros test. Cada uno podemos crear los nuestros, en mi caso, por ejemplo, empezando por el test para números primos; Para esta comprobación he generado la siguiente casuística:
procedure TestEsNumeroPrimo_0; procedure TestEsNumeroPrimo_1; procedure TestEsNumeroPrimo_2; procedure TestEsNumeroPrimo_3; procedure TestEsNumeroPrimo_4; procedure TestEsNumeroPrimo_20; procedure TestEsNumeroPrimo_11; procedure TestEsNumeroPrimo_18; procedure TestEsNumeroPrimo_17; procedure TestEsNumeroPrimo_331; procedure TestEsNumeroPrimo_5557; |
La implementación de estos test es la que se ve a continuación. Ahora debo definir el resultado correcto que yo espero en cada test, si la función TestEsNumeroPrimo funcionase correctamente. Para ello voy a utilizar las 2 funciones que me provee el framework de testing:
- CheckTrue
- CheckFalse
Como ya podéis imaginar por el nombre estas funciones, se usan para comprobar si un resultado de una función devuelve el resultado booleano esperado.
Recordemos que nuestra función para calcular si un número es primo (EsNumeroPrimo) todavía no está implementada y devuelve siempre True, así que es previsible que la ejecución de los test devuelve errores.
La implementación de los test es la siguiente (*NOTA1*):
procedure TestTAritmeticaBasica.TestEsNumeroPrimo_0; begin CheckFalse(FAritmeticaBasica.EsNumeroPrimo(0)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_1; begin CheckTrue(FAritmeticaBasica.EsNumeroPrimo(1)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_2; begin CheckTrue(FAritmeticaBasica.EsNumeroPrimo(2)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_3; begin CheckTrue(FAritmeticaBasica.EsNumeroPrimo(3)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_4; begin CheckFalse(FAritmeticaBasica.EsNumeroPrimo(4)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_11; begin CheckTrue(FAritmeticaBasica.EsNumeroPrimo(11)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_17; begin CheckTrue(FAritmeticaBasica.EsNumeroPrimo(17)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_18; begin CheckFalse(FAritmeticaBasica.EsNumeroPrimo(18)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_20; begin CheckFalse(FAritmeticaBasica.EsNumeroPrimo(20)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_331; begin CheckTrue(FAritmeticaBasica.EsNumeroPrimo(331)); end; procedure TestTAritmeticaBasica.TestEsNumeroPrimo_5557; begin CheckTrue(FAritmeticaBasica.EsNumeroPrimo(5557)); end; |
Para cada uno de los números utilizamos CheckTrue o CheckFalse dependiendo del resultado que deberíamos obtener.
Si ahora volvemos a ejecutar los test obtendremos el siguiente resultado.
NOTA: A priori a uno se le podría ocurrir programar los test para la función que comprueba si un número es primo de una forma similar a esta (*NOTA2*):
procedure TestTAritmeticaBasica.TestEsNumeroPrimo; begin CheckFalse(FAritmeticaBasica.EsNumeroPrimo(0)); CheckTrue(FAritmeticaBasica.EsNumeroPrimo(1)); CheckTrue(FAritmeticaBasica.EsNumeroPrimo(2)); CheckTrue(FAritmeticaBasica.EsNumeroPrimo(3)); CheckFalse(FAritmeticaBasica.EsNumeroPrimo(4)); CheckTrue(FAritmeticaBasica.EsNumeroPrimo(11)); CheckTrue(FAritmeticaBasica.EsNumeroPrimo(17)); CheckFalse(FAritmeticaBasica.EsNumeroPrimo(18)); CheckFalse(FAritmeticaBasica.EsNumeroPrimo(20)); CheckTrue(FAritmeticaBasica.EsNumeroPrimo(331)); CheckTrue(FAritmeticaBasica.EsNumeroPrimo(5557)); end; |
El problema es que de esta forma incumpliríamos las premisas de un test unitario que comentamos en la primera entrada de esta serie; Que un test «no debe tener dependencias»; De esta forma, cuando el primer test falle, ya no se comprobará ninguno más. Por lo tanto la ejecución de uno de estos test, depende de que los anteriores hayan funcionado correctamente.
Para corregir esto en DUnit, la única forma es parametrizar los test unitarios como veremos en el último punto de esta entrada.
Por último, y siguiendo el esquema que vemos a la derecha, ahora que ya hemos implementado los test, debemos implementar la función que estamos probando (EsNumeroPrimo).
Vosotros mismos podéis intentar implementarla y probar. En mi caso he utilizado/adaptado esta que me ha parecido simple y que he encontrado aquí.
function TAritmeticaBasica.EsPrimo(x:integer):boolean; var i,r : longint; begin r:=round(sqrt(x)); for i:=2 to r do if (x mod i=0) then begin esPrimo:=false; exit; end; esPrimo:=true; end; |
Ahora que ya tenemos nuestra función, podemos volver a ejecutar los test para comprobar la validez de nuestra implementación.
El resultado que obtenemos es el siguiente.
Como era de esperar, si nuestra función es correcta, los resultados del Test lo confirman.
Una vez que vemos las dos opciones anteriores nos damos cuenta que la primera opción (*NOTA1*) sería la correcta sintácticamente, pero poco práctica y la segunda (*NOTA2*) aunque parezca más óptima, no es correcta sintácticamente según TDD, dado que los Test deben ser independientes.
Una última opción sería “parametrizar” los test. En el caso de DUnit esta parametrización debemos hacerla nosotros, como ya veremos otros frameworks facilitan este trabajo.
CLASE PARA PAMETRIZAR TEST CON DUNIT
La idea en esta caso sería crear una clase que nos permita parametrizar la creación de test, de forma que podamos usar parámetros para crearlos y evitar así tener que hacerlo de forma manual.
Esta opción se ajusta más a las directrices de TDD y además nos evitará trabajo si el número de casos a probar en un test es grande, a cambio de incrementar un poco la complejidad de creación de los test.
Nuestra clase de parametrización de números primos tendrá la siguiente estructura:
// Clase para parametrizar los test de EsNumeroPrimo TestTAritmeticaBasicaEsNumeroPrimo = class(TTestCase) strict private FValue:Integer; FEsPrimo:boolean; FAritmeticaBasica: TAritmeticaBasica; public constructor Create(AValue: Integer; AEsPrimo:boolean); reintroduce; function GetName: string; override; procedure SetUp; override; procedure TearDown; override; published procedure Run; end; |
Y la de parametrizar test de factorización una muy similar:
// Clase para parametrizar los test de Factorizacion TestTAritmeticaBasicaFactorizacion = class(TTestCase) strict private FValue, FCount:Integer; FAritmeticaBasica: TAritmeticaBasica; public constructor Create(AValue: Integer; ACount:Integer); reintroduce; function GetName: string; override; procedure SetUp; override; procedure TearDown; override; published procedure Run; end; |
Podemos revisar la documentación de DUnit en Sourceforge y encontraremos la funcionalidad de los métodos que hemos definido:
-
SetUp: Se ejecuta antes de llamar al método que ejecuta el test (Run). De forma que lo utilizaremos para Inicializaciones.
-
TearDown: Se ejecuta después del método que ejecuta el test (Run) y normalmente lo utilizaremos para liberar recursos que hayamos podido crear en el método SetUp.
-
GetName: Nos permite configurar utilizando parámetros el nombre del test que posteriormente aparecerá en la ventana de resultados.
-
Create: Nos permite crear realmente cada uno de los test que vamos a parametrizar. Utilizando el constructor debemos pasar todos los datos necesarios para parametrizar nuestro test.
-
Run: Es el método que realmente ejecuta nuestro test.
- Una vez que ya tenemos la definición vemos la implementación de la clase:
// Constructor de la clase (Parámetros; Valor y resultado) constructor TestTAritmeticaBasicaEsNumeroPrimo.Create(AValue: Integer; AEsPrimo:boolean); begin inherited Create('Run'); FValue := AValue; FEsPrimo := AEsPrimo; end; // Nos devuelve el nombre para el test (parametrizado con el valor) function TestTAritmeticaBasicaEsNumeroPrimo.GetName: string; begin Result := Format('TestEsNumeroPrimo_Numero_%.3d', [FValue]); end; // Método que ejecuta el test procedure TestTAritmeticaBasicaEsNumeroPrimo.Run; var b:boolean; begin // Ejecutar y comprobar b := FAritmeticaBasica.EsNumeroPrimo(FValue); Check((b = FEsPrimo), Format('Se esperaba que el número %d<%D> devolviera un resultado %s<%S>.',[FValue, BoolToStr(FEsPrimo)])); end; // preparar el test y crear los elementos necesarios procedure TestTAritmeticaBasicaEsNumeroPrimo.SetUp; begin inherited; FAritmeticaBasica := TAritmeticaBasica.Create; end; // Finalizar y liberar recursos creados procedure TestTAritmeticaBasicaEsNumeroPrimo.TearDown; begin FreeAndNil(FAritmeticaBasica); inherited; end; |
- Por último falta definir el procedimiento que registra todos los test que necesitemos para esta clase utilizando parámetros.
procedure RegisterTestEsNumeroPrimo; begin // Registrar test parametrizados de EsNumeroPrimo RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(0, False)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(1, True)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(2, True)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(3, True)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(4, False)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(11, True)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(17, True)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(18, False)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(20, False)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(331, True)); RegisterTest('TestEsNumeroPrimo', TestTAritmeticaBasicaEsNumeroPrimo.Create(5557, True)); end; |
- Si utilizando esta nueva clase, ejecutamos los test, veremos el resultado tal y como esperamos.
Hasta aquí esta entrada en la que hemos visto cómo crear test unitarios con el Framework DUnit. En la siguiente haremos algo similar con DUnitX y veremos algunas diferencias entre ellos.
Os dejo el código fuente completo de los proyectos.
Como siempre, cualquier comentario, sugerencia, aportación,… será bienvenida.
Hasta la próxima.
Embarcadero MVP.
Analista y Programador de Sistemas Informáticos.
Estudios de Informática (Ingeniería Técnica Superior) en la UPC (Universidad Politécnica de Barcelona).
Llevo utilizando Delphi desde su versión 3. Especialista en diseño de componentes, Bases de Datos, Frameworks de Persistencia, Integración Continua, Desarrollo móvil,…
Muy interesante, sobre todo si trabajas utilizando la metodología ágil llamada DDT (https://es.wikipedia.org/wiki/Data-driven_testing ) .
Gracias por recordarnos la forma de realizar test unitarios con Delphi.
@Emilio
Hola Emilio.
La idea es mostrar que no es tan difícil o complicado como de primeras puede sonar.