Ejemplo sencillo con Mockito

De ChuWiki
Saltar a: navegación, buscar

Cuando tenemos clases java que tienen dependencias de otras clases muy complejas y queremos hacer test de Junit de ellas, podemos encontrarnos con que tenemos que hacer todo un montaje para poder construir y que funcione la clase que queremos testear. Aquí es donde Mockito vienen en nuestra ayuda. Mockito puede simular esas clases complejas que necesita nuestra clase para que podamos construirla sin tanto montaje.

Veamos un ejemplo de qué queremos decir. El ejemplo completo lo tienes en mockito-example

La clase compleja

Imagina que tenemos una clase para testear. Esta clase lee un String de base de datos, se conecta luego a un servidor remoto para leer otro String, los concatena y lo saca por pantalla usando otra clase. La clase es como esta

package com.chuidiang.mockito_examples;

/**
 * @author fjabellan
 * @date 15/11/2020
 */
public class SomeComplexClass {
    private DataBaseClass dataBaseClass;
    private NetworkClass networkClass;
    private OutputClass outputClass;

    public SomeComplexClass(DataBaseClass dataBaseClass, NetworkClass networkClass, OutputClass outputClass){
        this.dataBaseClass = dataBaseClass;
        this.networkClass = networkClass;
        this.outputClass = outputClass;
    }

    public void concatStringsFromDataBaseAndNetworkAndOutputResult(){
        try {
            final String stringFromDataBase = dataBaseClass.getStringFromDataBase();
            final String stringFromRemoteServer = networkClass.getStringFromRemoteServer();
            outputClass.printOutput(stringFromDataBase+" - "+stringFromRemoteServer);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

El código aquí en sí es sencillo, pedir un par de String a un par de clases, concatenarlo y sacarlo con una tercera clase. Si miras las clases DataBaseClass.java, NetworkClass.java y OutputClass.java, verás que más o menos están hechas de verdad, establecen una conexión con base de datos para hacer una consulta, abren un socket para leer una cadena de texto y sacan por pantalla el texto.

Hacer un test de Junit de esto, de la forma "tradicional", sería compolejo, porque tendríamos que tener levantada una base de datos con una cadena específica que esperemos en el test, una servidor de socket al que podamos conectarnos y nos de la cadena que esperamos y finalmente, algo para mirar qué es lo que outputClass saca por pantalla.

Punto importante para poder hacer test de JUnit

El primer paso para la solución, ya lo hemos tomado. Esta clase NO debe hacer los new de las clases de base de datos, de network o de output. Si hace ella misma el new, ya no tenemos nada que hacer. La solución parte por hacer lo que hemos hecho aquí, que reciba esas clases bien en el constructor, bien en método set().

Esto es importante si quieres hacer test unitarios. Separa en clases cualquier cosa que sea "compleja" de simular en un test de Junit, como base de datos o temas de socket. Deja en esas clases lo mínimo posible y haz que tus clases con lógica las reciban bien en el constructor, bien en método set().

Hacer Mocks de las clases

Una vez vez que tenemos esto así, el test de JUnit ya se facilita. En vez de pasar a nuestra clase las clases de base de datos o network, podemos pasarle clases hija de estas en las que sobre escribimos los métodos para que nos den los datos que esperamos para test. Estas clases hijas se llaman mock object. Por ejemplo, para la de base de datos podemos hacer

<syntaxhihghlight lang="java"> public class DataBaseMock extends DataBaseClass {

  @Override
  public String getStringFromDataBase(){
     return "Hello";
  }

} </syntaxhihglight>

La clase de Network sería similar.

Para la clase Output sería algo un poco más complejo, porque queremos saber qué texto le envía nuestra clase para poder verificarlo. Así que nuestro OutputMock debería guardarse el String recibido de forma que luego podamos verificarlo

public class OutputMock extends OutputClass {
   private String theString;

   public String getTheString() {
      return theString;
   }

   public void printOutput(String theString){
      this.theString=theString;
   }
}

Así, en nuestro test de JUnit, instanciamos estos Mock, instanciamos la clase bajo test pasándole los mock y ya tenemos los datos deseados. Para verificar que todo va bien, a nuestra clase OutputMock le pediríamos con getTheString() cual es el resultado para ver si es el esperado.

Mockito

Si haces muchos test, escribir estos mock object es tedioso. Y para no repetir una y otra vez las mismas clases, se empiezan a complicar los mock. Por ejemplo, igual que al OutputMock le hemos hecho guardarse el string para luego poder acceder a él, podemos necesitar que DataBaseMock y NetworkMock den cadenas distintas en distintos test. Así que igual hay que modificarlos para decirles qué cadena deben devolver en cada momento.

Mucho código repetitivo o complejo solo para hacer test.

Y aquí es donde mockito entra en nuestra ayuda. Te pongo el test de Junit hecho con mockito y vamos explicando cosas

package com.chuidiang.mockito_examples;


import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;

import java.io.IOException;
import java.sql.SQLException;

/**
 * @author fjabellan
 * @date 15/11/2020
 */
 @RunWith(MockitoJUnitRunner.class)
public class SomeComplexClassTest {
    @InjectMocks
    SomeComplexClass someComplexClass;

    @Mock
    DataBaseClass dataBaseClass;

    @Mock
    NetworkClass networkClass;

    @Mock
    OutputClass outputClass;

    @Test
    public void someSimpleTest() throws SQLException, ClassNotFoundException, IOException {
        Mockito.when(dataBaseClass.getStringFromDataBase()).thenReturn("Hello");
        Mockito.when(networkClass.getStringFromRemoteServer()).thenReturn("World");

        someComplexClass.concatStringsFromDataBaseAndNetworkAndOutputResult();

        Mockito.verify(outputClass).printOutput("Hello - World");

    }
}

Primero, nuestra clase de test la anotamos con @RunWith(MockitoJUnitRunner.class). Esto hace que mockito entre en acción para este test. Si no lo ponemos, no se hará caso de todo lo que pongamos de mockito en el test.

Las clases de las que queramos mock object, es decir, DataBaseClass, NetworkClass y OutputClass, las declaramos como atributos de la clase de test y las anotamos como @Mock. Esto hará que mockito genere automáticamente para ellas mock objects que podemos configurar durante el test. No es necesario que hagamos new ni que escribamos nada de código.

La clase de la que queremos hacer el test la anotamos con @InjectMocks. Esto hará que mockito directamente la instancie y le pase los mock object. Mockito es listo, busca los constructores que admitan esas clases pasa pasárselos o los atributos dentro de la clase para darles valor. Se fia primero del tipo. Si hubiera varios atributos del mismo tipo, entonces por el nombre de la variable. No obstante, con mockito hay formas de afinar todo esto manualmente. En este ejemplo hemos ido a lo sencillo, cada atributo de la clase es de un tipo distinto y hemos puesto un constructor que admite las tres cosas que necesita la clase.

No necesitamos instanciar la clase bajo test. Mockito lo hace por nosotros. En cada test, tendremos mocks nuevos y clase bajo test nueva.

Nos metemos ya en el test. Lo primero es configurar los mock para que devuelvan lo que queramos. Las líneas

        Mockito.when(dataBaseClass.getStringFromDataBase()).thenReturn("Hello");
        Mockito.when(networkClass.getStringFromRemoteServer()).thenReturn("World");

le está diciendo a Mockito que cuando se llame al método dataBaseClass.getStringFromDataBAse(), entonces debe devolver "Hello". La sintaxis es bastante intuitiva. Para la parte network no hay mucho más que explicar, es similar.

Luego, nuestro test, ya puede llamar a la clase bajo test y a su método concatStringsFromDataBaseAndNetworkAndOutputResult()

¿Cómo miramos ahora el resultado?. Pues ese método debería llamar a outputClass.printOutput() y pasarle como parámetro "Hello - World". Así que le preguntamos a mockito si ha sido así, puesto que tenemos el outputMock.

        Mockito.verify(outputClass).printOutput("Hello - World");

Le decimos que verifique que a outputClass (el mock) se le ha llamado al método printOutput() pasando "Hello - World". Si ha sido así, todo correcto, si no ha sido así, falla el test de Junit.

En resumen, nos hemos ahorrado escribir los mock a mano y de forma sencilla hemos hecho nuestro test.

Más posibilidades de mockito

Lo dejamos para un próximo tutorial, pero mockito nos ofrece muchas más posibilidades. Contamos algunas de las más usadas.

En nuestro ejemplo hemos que hecho que las llamadas a los métodos de los mock devuelvan un valor concreto. Es posible que dicho método tenga parámetros y queramos devolver cosas distintas según los parámetros. Mockito nos permite hacer este tipo de cosas. Por ejemplo

Mockito.when(mockedList.get(0)).thenReturn("first");
Mockito.when(mockedList.get(1)).thenThrow(new RuntimeException());

Si nuestro mock es una lista, cuando se le llame con get(0 devolverá "first" y si se le llama con get(1), lanza una excepción.

Tampoco necesitamos poner valores concretos en los parámetros, se pueden poner "comodines" de esta forma

Mockito.when(mockedList.get(Mockito.anyInt())).thenReturn("element");

Este por ejemplo devuelve "element" siempre que se llame a get() pasando un entero.

También es posible capturar con qué parámetros han llamado a nuestro mock. En el ejemplo de outputClass, se podría poner

        Mockito.when(dataBaseClass.getStringFromDataBase()).thenReturn("Hello");
        Mockito.when(networkClass.getStringFromRemoteServer()).thenReturn("World");

        someComplexClass.concatStringsFromDataBaseAndNetworkAndOutputResult();

        ArgumentCaptor<String> argument = ArgumentCaptor.forClass(String.class);
        Mockito.verify(outputClass).printOutput(argument.capture());

        Assert.assertEquals("Hello - World",argument.getValue());

Hemos creado un ArgumentCaptor de String antes de llamar al Mockito.verify(). En el método, en vez de poner directamente la cadena "Hello - Worl", ponemos argument.capture(), esto nos dará acceso a la cadena con la que se ha llamado a outputClass. Y con ello podemos hacer nuestras verificaciones de la forma habitual en mockito.

Para un simple String esto no es muy útil, pero si nuestro método printOutput admitiera como parámetro una clase más compleja que un String, con varios atributos y tal, sería difícil verificar que todo es correcto directamente en Mockito.verify(). Así que ahí si es útil un ArgumentCaptor, para obtener el parámetro usado con todos sus campos y luego usar los métodos de verificación usuales que queramos.

Dependencias gradle

Aparte de la dependencia de JUnit, para tener mockito debermos añadir dos dependencias

    testCompile group: 'org.mockito', name: 'mockito-core', version: '3.6.0'
    testCompile group: 'org.mockito', name: 'mockito-junit-jupiter', version: '3.6.0'

La primera es para tener la magia de mockito. La segunda solo si usamos junit. Es para que mockito y junit enganchen y así, los Mockito.verfify() que fallen avisen a JUnit de que hay fallo.