Ejemplo sencillo con Mockito
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 bajo test[editar]
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 complejo, 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[editar]
El primer paso para la solución, ya lo hemos tomado. Esta clase no hace los new de las clases de base de datos, de network o de output. Si hace ella misma el new, depende de cómo lo haga. Si los hace en el constructor o directamente en la declaración del atributo, también puede funcionar mockito. Pero si hace los new entre medias del código de otros métodos, machacará nuestros mock. En cualquier caso, suele ser buena constumbre 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[editar]
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
public class DataBaseMock extends DataBaseClass {
@Override
public String getStringFromDataBase(){
return "Hello";
}
}
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[editar]
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.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import java.io.IOException;
import java.sql.SQLException;
/**
* @author chuidiang
* @date 15/11/2020
*/
//@RunWith(MockitoJUnitRunner.class)
@ExtendWith(MockitoExtension.class)
public class SomeComplexClassTest {
@InjectMocks
SomeComplexClass someComplexClass;
@Mock
DataBaseClass dataBaseClass;
@Mock
NetworkClass networkClass;
@Mock
OutputClass outputClass;
@Test
public void aSimpleTest() 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)
si estamos con JUnit 4 o con JUnit 5 usando junit-vintage-engine@ExtendWith(MockitoExtension.class)
si estamos con JUnit 5 y junit-jupiter-engine.
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[editar]
Puedes ver que, aparte de la creación de mocks, los dos métodos principales de Mockito son
- Mockito when para indicar cómo se debe comportar el mock
- Mockito verify para verificar qué se ha hecho con el mock.
En los enlaces puedes ver más detalles sobre estos métodos.