domingo, 29 de septiembre de 2019

Pruebas software. Dobles de prueba con ejemplos (Java, Junit y Mockito)

Porqué existen dobles de pruebas


Las pruebas automatizadas son imprescindibles para el desarrollo de  software. Existen distintos tipos y niveles de pruebas. Con tipos de pruebas me refiero a: de funcionalidad, de rendimiento, de usabilidad, etc. Con niveles a: unitarias, integración, sistema, aceptación y otras más que se puedan encontrar en la literatura al respecto.

Todos los tipos y niveles de pruebas son susceptibles de automatización. No obstante tal y como indica Lisa Crispin en Los cuadrantes del testing agile unos tipos y/o niveles de pruebas son más proclives a la automatización que otros. Esto no es objetivo de este artículo.

Para explicar lo que es un doble de prueba, vamos a centrarnos inicialmente en las pruebas unitarias automatizadas. Las pruebas unitarias están ideadas para verificar que cada pequeña pieza de código que constituye un sistema, hace lo que tiene que hacer. El nombre que recibe una pieza de código depende del lenguaje de programación y de la arquitectura software que se esté utilizando. Puede ser una clase, función, método, componente, etc. Los sistemas están formados por cientos sino miles de piezas, que interactuando dan soporte a las funcionalidades del sistema. Por ello al probar una pieza estamos probando la propia pieza y todas las piezas con las que interactúa y este no es el objetivo de una prueba unitaria.

Con el objetivo de aislar una pieza del resto de las piezas con las que interactúa para probarla de forma aislada, se ideó el concepto de dobles de test.  Un doble de test es una clase específica para pruebas que sustituye a una pieza real que interacciona con la pieza que se está probando.

Para ilustrar mejor los conceptos veamos el siguiente ejemplo.

Se dispone de un sistema de gestión de pedidos que tiene 4 piezas de software. La que se quiere probar es la pieza Factura, por eso está marcada en la figura como SUT que es el acrónimo de  Subject Under Test. Se quiere hacer una prueba unitaria de Factura. Factura interacciona con las piezas Cliente y LineaFactura, además a través de LineaFactura con Producto. Para realizar una prueba unitaria podemos utilizar dobles de prueba para estas tres piezas, consiguiendo de esta forma aislar la prueba de Factura de la prueba de Cliente, Producto y LineaFactura. Esta es la misón de los dobles de prueba que suplantan a las piezas originales durante la ejecución de la prueba unitaria de Factura.

Frameworks de dobles de pruebas

Con el objetivo de poder crear dobles de prueba de la forma más sencilla para los programadores se han creado múltiples Frameworks de dobles de pruebas. Estos Frameworks en su gran mayoria están implementados de acuerdo con la corriente XUnit . Estos Frameworks la mayor parte de las veces en forma de librerías, aportan funciones que realizan los pasos: Preparar prueba, ejecutar pieza a probar, validar resultado de la ejecución.

Para aterrizar lo expuesto vamos a ver un ejemplo que usa Mockito que es un framework de dobles de prueba para java, cómo framework de pruebas para java se usa JUnit.


    01 @Test
    02 public void facturaAnadirLinea(){

    03
    04    // Preparar prueba
    05    final int CANTIDAD = 4;
    06    Producto productoMock = Mockito.mock(Producto.class);          
    07    Cliente clienteMock = Mockito.mock(Cliente.class);
    08    Factura factura = new Factura(clienteMock);
    09    LineaFactura lineaFactura = new LineaFactura(productoMock, CANTIDAD);
    10          
    11    // Ejecutar pieza a probar (SUT)
    12    factura.anadirLinea(lineaFactura);      
    13    
    14    // Validar resultado
    15    List lineas = factura.obtenerLineas();
    16    assertEquals(lineaFactura, lineas.get(0));
    17    assertEquals(lineas.size(), 1);
    18 }

   
En este ejemplo en las líneas 06 y 07 se crean objetos que no son instancias de los tipos reales. En su lugar son impostores o dobles que simulan que son los originales sin serlo. De esta forma se puede probar el método anadirLinea de Factura sin crear objetos auténticos de los tipos Producto ni Cliente. Lo mismo se podría hacer con la lineaFactura, en cuyo caso el código quedaría.

   01 @Test
   02 public void facturaAnadirLineaHayStock(){
   03    
   04     // Preparar
   05     Cliente clienteMock = Mockito.mock(Cliente.class);
   06     Factura factura = new Factura(clienteMock);
   07     LineaFactura lineaFactura = Mockito.mock(LineaFactura.class);
   08            
   09     // Ejecutar pieza a probar (SUT)
   10     factura.anadirLinea(lineaFactura);       
   11      
   12     // Validar resultado
   13     List lineas = factura.obtenerLineas();
   14     assertEquals(lineaFactura, lineas.get(0));
   15     assertEquals(lineas.size(), 1);
   16 }
   
 
En este caso, ni siquiera es necesario crear un objeto de tipo Producto, puesto que eso solo es necesario para crear objetos LineaFactura auténticos. 

Tipos de dobles de pruebas 

Ahora vamos a diferenciar los dobles de pruebas en dos tipos, unos los que permiten validar el estado y los otros los que permiten validar el comportamiento.

El ejemplo que hemos visto controla el estado del objeto. Esto es porque lo que verifica es que después de añadir una línea a una factura en la lista de líneas de factura haya al menos una línea. Esto forma parte del estado de la Factura que tiene 0, 1 o varias LineasFacturas. En este caso 1 LineaFactura.

Para entender los dobles de prueba que validan el comportamiento vamos a ver un ejemplo.
Se trata de preparar la prueba unitaria para Pedido. El pedido puede ser servido o no dependiendo de que haya unidades suficientes en el almacén. En caso de que no haya unidades se envía un correo al responsable del almacén para que reponga producto.

El código de Pedido es

01 package mocksversusstubs;
02
03 public class Pedido {
04    private ServicioCorreo pMailer;
05    private String pNombre;
06    private int pNumero;
07
08    public Pedido(String nombre, int numero, ServicioCorreo mailer){
09        pNombre = nombre;
10        pNumero = numero;
11        pMailer = mailer;
12    }
13
14    public void rellena(Almacen central){
15        if(central.tieneInventario(pNombre, pNumero)){
16            pNumero = 0;
17        }else{
18            if(null != pMailer)
19                pMailer.envia("No hay stock");
20        }
21    }
22  
23    public boolean estaRelleno(){
24        return 0 == pNumero;
25    }
26 }


El código de Almacén

01 package mocksversusstubs;
02
03 import java.util.HashMap;
04
05 public class Almacen {
06    private HashMap pCantidad =
07            new HashMap();
08
09    public boolean tieneInventario(String nombre, int numero){
10        pCantidad.put(nombre, 50);
11        int resto = tomaInventario(nombre);
12
13        if(numero <= resto){
14            pCantidad.put(nombre, resto-numero);
15            return true;
16        }
17        return false;
18    }
19
20    public void anade(String nombre, int numero){
21        pCantidad.put(nombre, numero);
22    }
23
24    public int tomaInventario(String nombre){
25        Integer resto = pCantidad.get(nombre);
26        return resto == null ? 0 : resto;
27    }
28 }


El código de ServicioCorreo

01 package mocksversusstubs;
02
03 public class ServicioCorreo {
04
05    public String envia(String str){
06        return str;
07    }
08 }


Finalmente el código de la prueba unitaria de Pedido

01 package mocksversusstubs;
02
03 import static org.junit.Assert.*;
04 import org.junit.Test;
05 import org.mockito.Mock;
06 import org.mockito.Mockito;
07
08 public class TesterDeInteraccionConPedido{
09
10    private static String TALISKER = "Talisker";
11    @Mock Almacen almacenMock;
12    @Mock ServicioCorreo emailMock;
13
14    @Test
15    public void testHacerPedidoQuitaDelInventarioSiEstaEnStock() {
16       //setup - data
17       emailMock = Mockito.mock(ServicioCorreo.class);
18       Pedido pedido = new Pedido(TALISKER, 50, emailMock);       
19       almacenMock = Mockito.mock(Almacen.class);
20       //setup - expectations
21       Mockito.when(almacenMock.tieneInventario(TALISKER, 50))
22              .thenReturn(true);
23       //exercise
24       pedido.rellena(almacenMock);
25       //verify           
26       Mockito.verify(almacenMock).tieneInventario(TALISKER, 50);
27       assertTrue(pedido.estaRelleno());
28    }
29
30    @Test
31    public void testHacerPedidoNoQuitaSiNoHaySuficienteEnStock() {
32       //setup - data
33       emailMock = Mockito.mock(ServicioCorreo.class);
34       Pedido pedido = new Pedido(TALISKER, 51, emailMock);
35       almacenMock = Mockito.mock(Almacen.class);
36       //setup - expectations
37       Mockito.when(almacenMock.tieneInventario(TALISKER, 51))
38              .thenReturn(false);
39       Mockito.when(emailMock.envia("No hay stock"))
40              .thenReturn("No hay stock");
41       //exercise
42       pedido.rellena(almacenMock);
43       //verify
44       Mockito.verify(almacenMock).tieneInventario(TALISKER, 51);
45       Mockito.verify(emailMock).envia("No hay stock");
46       assertFalse(pedido.estaRelleno());
47    }
48
49 }


En este ejemplo se utilizan dobles de prueba para los dos tipos que no se están probando, Almacén y ServicioCorreo. En la línea 21 y 22 se dice al doble de Almacén como debe comportarse cuando se invoque al método tieneInventario con los argumentos (TALISKER, 50), determinando que devuelva true para simular que de el producto TALISKER hay al menos 50 unidades. La verificación de comportamiento está en la línea 26, donde se comprueba que Pedido llama al método tieneInventario de Almacén con los argumentos (TALISKER, 50). En la línea 27 se comprueba el estado de pedido, en este caso true.

También en las líneas 44 y 45 se comprueba el comportamiento de Pedido en cuanto a que usa los métodos tieneInventario de Almacén y envia de Email. En la línea 46 se comprueba el estado de Pedido.

Conclusiones

Los dobles de prueba son de uso imprescindible para aislar la prueba unitaria de una pieza de software de las piezas con las que interacciona. 

Utilizar frameworks de pruebas ayuda a crear los dobles eliminando la necesidad de crearlos desde cero por el programador como tipos independientes que solo sirven a efectos de realizar las pruebas.

También es importante realizar el diseño de sistemas que sean testeables.  Los sistemas no testeables mediante pruebas unitarias automatizadas son más difíciles de modificar y son supceptibles de fallar puesto que las pruebas de regresión son complicadas de realizar.

Una forma de conseguir sistemas testeables es desarrollarlos con la técnica Test Driven Development (TDD), pero esa es otra historia.

No hay comentarios:

Publicar un comentario