Páginas

Mostrando entradas con la etiqueta mockito. Mostrar todas las entradas
Mostrando entradas con la etiqueta mockito. Mostrar todas las entradas

lunes, 7 de octubre de 2019

Ejemplo de Verify FindOne() de MongoCollection de Jongo con JUnit y Mockito

Planteamiento y código

Este ejemplo está elaborado con Java 1.5, JUnit 4.12, Mockito 1.9.5, MongoDB driver 3.10.2, Jongo 1.3.0 y la base de datos MongoDB server 4.0.

Se muestra cómo preparar una prueba para un módulo de acceso a base de datos. Lo más remarcable es la forma de acceder a verificar que el método FindOne(query) es invocado una vez.
El siguiente módulo accede a una base de datos MongoDB, para buscar un usuario y comprobar si su contraseña coincide con una dada que contiene el objetoUsuarioBean. 

El módulo que accede a la base de datos y es el objeto de la prueba. O sea el SUT (Subject Under Test)

UsuarioColeccion.java

import org.jongo.Jongo;
import org.jongo.MongoCollection;
import com.mongodb.DB;
import com.mongodb.MongoClient;

public class UsuarioColeccion {
    private DB db;
    private MongoCollection mongoCollection;
   
    public MongoCollection getMongoCollection() {
      return mongoCollection;
    }
   
    public void setMongoCollection(MongoCollection mongoCollection){
        this.mongoCollection = mongoCollection;
    }

    @SuppressWarnings("deprecation")
    public UsuarioColeccion()  {
        db = new MongoClient().getDB("UsuariosDB");
        mongoCollection = new Jongo(db).getCollection("Usuarios");       
    }
        

    public boolean buscarUsuarioContrasena(UsuarioBean bean){ 
            String query="{_id: '"+bean.getNombreUsuario()+"'}";   
            UsuarioBean usuario = 

                 getMongoCollection().findOne(query).as(UsuarioBean.class);      
            if (usuario==null)
                return false;
            else           
                return usuario.getContraseña().equals(bean.getContraseña());
    }
}


El código del bean

UsuarioBean.java

import org.jongo.marshall.jackson.oid.Id;

@SuppressWarnings("deprecation")
public class UsuarioBean {
  
    @Id
    private String nombreUsuario;
    public String getNombreUsuario() {
        return nombreUsuario;
    }

    private String contraseña;
    public String getContraseña() {
        return contraseña;
    }

    public UsuarioBean() { }
    public UsuarioBean(String nombreUsuario, String contraseña) {
        this.nombreUsuario = nombreUsuario;
        this.contraseña = contraseña;       
    }
}
 


Se construye una prueba unitaria que no accede a la base de datos, este acceso se comprobará en las pruebas de integración. La prueba unitaria verifica que se ejecuta el método FindOne() de MongoCollection de Jongo una sola vez cuando se accede a buscar un documento en la base de datos. 

El módulo de prueba unitaria es

UsuarioColeccionTest.java

import java.net.UnknownHostException;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;

import org.jongo.FindOne;
import org.jongo.MongoCollection;

import estratecno.Mongo.UsuarioBean;
import estratecno.Mongo.UsuarioColeccion;

public class UsuarioColeccionTest {
   
    UsuarioColeccion coleccionUsuarios;
    UsuarioBean bean;
    MongoCollection mongoCollection;
   
    @Before
    public void before() throws UnknownHostException {
       coleccionUsuarios = new UsuarioColeccion();   
       bean = new UsuarioBean("Pepe","abcd");
       mongoCollection = Mockito.mock(MongoCollection.class);
    } 


    @Test
    public void cuandoBuscaUsuarioUsaFindOne(){
        String query="{_id: '"+bean.getNombreUsuario()+"'}";
        FindOne findResult = Mockito.mock(FindOne.class);
        Mockito.doReturn(findResult).when(mongoCollection).findOne(query);
        coleccionUsuarios.setMongoCollection(mongoCollection);
        coleccionUsuarios.buscarUsuarioContrasena(bean);       
        Mockito.verify(mongoCollection, Mockito.times(1)).findOne(query);       
    }

}
 

Asuntos remarcables del ejemplo:

  • Aunque UsuarioColección crea su propia MongoCollection en el constructor,  la prueba unitaria mockea una MongoCollection que le inyecta a UsuarioColeccion. Esta MongoCollection que suplanta a la real, permite hacer la prueba sin acceder a la Base de Datos Real. La inyección se hace con:       
        coleccionUsuarios.setMongoCollection(mongoCollection);
  • Para decirle al suplantador que la MongoCollection auténtica llama al método findOne()  se codifican las expectativas de Mockito:
        FindOne findResult = Mockito.mock(FindOne.class);
        Mockito.doReturn(findResult).when(mongoCollection).findOne(query);


         En este caso la expectativa hace uso de un suplantador de FindOne llamado findResult. 
         findResult es necesario para crear la  expectativa como  tipo  que  devuelve  el  método 
         findOne(query).
  • La forma de verificar que se llama a findOne() una sola vez es:
        Mockito.verify(mongoCollection, Mockito.times(1)).findOne(query);



 

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.