Inicio > Código, Delphi, Test > Test Unitarios; Framework DUnit (Entrega 2)

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.

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.

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.

  1. 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.
  2. 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.

Ejecucion_Test_NumeroPrimo

 

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;

tdd_cycle

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.

Resultado_Test_Unitarios

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.

Resultado

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.

 

Vota este post
Categories: Código, Delphi, Test Tags: , ,
  1. domingo, 4 de febrero de 2018 a las 20:19 | #1

    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.

  2. lunes, 5 de febrero de 2018 a las 10:10 | #2

    @Emilio
    Hola Emilio.
    La idea es mostrar que no es tan difícil o complicado como de primeras puede sonar.

  1. Sin trackbacks aún.