Test Doubles con Mockito

Última actualización: 16/05/2021

mockito

Cuando testeamos una clase, nos encontraremos a menudo con que posee una o varias dependencias de otras en las que se apoya y que, por tanto, condicionan el comportamiento del código a probar. A veces, esas dependencias nos impiden la realización de los tests, ya que pueden conectarse a servicios REST, bases de datos, etc., sobre los que no tenemos el control.

Un test double o doble de test es un objeto que reemplaza a uno equivalente de la aplicación real y cuyo comportamiento podemos configurar a nuestra conveniencia, según lo necesitemos para probar los objetos que dependen de él. De este modo, se aísla el código que estamos testeando del resto de la aplicación, algo fundamental en el que caso de que estemos escribiendo pruebas unitarias, y además podemos probarlo a fondo, simulando si hiciera falta múltiples comportamientos del test double (por ejemplo, devolver ciertos resultados, lanzar excepciones, etc.).

La siguiente figura ilustra el concepto con un caso típico.

Queremos probar una clase que a su vez realiza llamadas a otra que interacciona con una base de datos. Esto plantea un problema por partida doble si la estamos probando de forma aislada.

  • Necesitamos que la base de datos esté disponible.

  • La base de datos debe ofrecer para cada prueba un juego de datos adecuado que nos permita probar lo que queremos.

¿La solución? Usar en las pruebas objetos de la clase DAO con métodos personalizados que hagan lo que necesitemos.

A los dobles de test se les suele denominar genéricamente mocks (bocetos) pero, siendo estrictos, se clasifican en cinco tipos:

  • Dummy: objetos que necesitamos para ejecutar el test aunque no hacen nada, o da lo mismo lo que hagan porque no afectan a la prueba.
  • Stub: es como un dummy y sus métodos no hacen nada, pero devuelven cierto valor predeterminado.
  • Spy: permite “espiar” el uso que se hace del propio objeto, por ejemplo llevar el conteo del número de veces que se ejecuta un método, los argumentos que se le van pasando…
  • Mock: es un stub en el que sus métodos sí implementan un comportamiento. Reciben unos valores y en función de ellos se devuelve una respuesta.
  • Fake: es un objeto “correcta y completamente” implementado y que equivalente al objeto real al que simula. Sin embargo, falsea algún comportamiento que no puede ser aplicado en los tests. Por ejemplo, pensemos en la implementación de un DataSource que en lugar de conectarse a una base de datos MySQL tal y como hace el datasource de la aplicación real, se conecta a una base de datos embebida en memoria para que los tests sean más rápidos y portables.

Volvamos a la ilustración anterior. Si la clase DAO implementa una interfaz, lo cual a veces no va a suceder, crear dobles de tests es sencillo: podemos crear implementaciones personalizadas y exclusivas para las pruebas. Pero esto resultará tedioso y con frecuencia terminaremos creando numerosas clases.

Con la librería de código abierto Mockito es posible modificar de forma dinámica el comportamiento de métodos concretos de un objeto, o crear dobles de tests al completo sin tener que escribir ni una sola clase. Todo esto entre muchas otras características.

Proyecto de ejemplo

Vamos a “jugar” con las posibilidades de Mockito testeando con JUnit 4 un par de clases muy sencillas relacionadas entre sí.

package com.danielme.blog.testdouble;

public class Dependency {

	private final SubDependency subDependency;

	public Dependency(SubDependency subDependency) {
		super();
		this.subDependency = subDependency;
	}

	public String getClassName() {
		return this.getClass().getSimpleName();
	}

	public String getSubdepedencyClassName() {
		return subDependency.getClassName();
	}

	public int addTwo(int i) {
		return i + 2;
	}

	public String getClassNameUpperCase() {
		return getClassName().toUpperCase();
	}

}

package com.danielme.blog.testdouble;

public class SubDependency {

	public String getClassName() {
		return this.getClass().getSimpleName();
	}
}

Mockito y @Mock

Para usar Mockito en nuestros proyectos Maven solo tenemos que incluir una dependencia; no se requiere de ninguna configuración. Usamos el ámbito test para que la librería no forme parte del empaquetado final del proyecto.

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>${mockito.version}</version>
    <scope>test</scope>
</dependency>

Empecemos construyendo una clase con pruebas en la que vamos a modificar “sobre la marcha” el comportamiento de los métodos de Dependency.

  1. Definimos la clase cuyo comportamiento queremos redefinir como un atributo anotado con @Mock
    @Mock
    private Dependency dependency;
    
  2. Hacemos que Mockito instancie los objetos anotados con @Mock antes de la ejecución de cada prueba. La idea es que en cada una de ellas tengamos un objeto “limpio” que modificaremos a nuestro antojo.
    	@Before
    	public void setupMock() {
    		MockitoAnnotations.openMocks(this);		
    	}
    

    Si queremos crear de forma explícita un mock en cualquier punto del código, podemos hacerlo llamando a Mockito.mock.

        @Before
    	public void setupMock() {
    		dependency = mock(Dependency.class);	
    	}
    

    Otra opción, válida solo en JUnit 4, es hacer que Mockito instancie de forma automática los atributos @Mock utilizando una regla.

    @Rule 
    public MockitoRule mockitoRule = MockitoJUnit.rule();
    

Sea cual sea la alternativa elegida, ya tenemos el atributo dependency con una instancia de tipo mock lista para ser utilizada. Si inspeccionamos este objeto con un depurador, veremos que se trata de un Proxy, técnica empleada en marcos de trabajo tan importantes como Spring, Hibernate o Jakarta CDI. En el código, no hay ninguna diferencia entre el objeto proxy y el objeto al que representa, así que lo usamos normalmente.

De acuerdo a la clasificación indicada algunos párrafos atrás, nos encontramos ante un dummy. Todas las llamadas a sus métodos devuelven null tal y como verifica el siguiente test.

    @Test
	public void testDummy() {
		assertNull(dependency.getClassName());
		assertNull(dependency.getClassNameUpperCase());
		assertNull(dependency.getSubdepedencyClassName());		
	}

Lo interesante es definir el comportamiento de un método del mock.

@Test
public void testDependency() {		
	when(dependency.getClassName()).thenReturn("hi there");

	assertEquals("hi there", dependency.getClassName());
}

La API es tan intuitiva que el código anterior no requiere de grandes explicaciones. Hemos escrito que “cuando se invoque al método getClassName del objeto dependency, se debe devolver la cadena “hi there””. Todo ello con una sola línea de código gracias al método estático when.

También se puede forzar el lanzamiento de una excepción, lo que nos permite probar la gestión de errores de nuestro código.

@Test(expected = IllegalArgumentException.class)
public void testException() {		
	when(dependency.getClassName()).thenThrow(IllegalArgumentException.class);

	dependency.getClassName();
}

Si el método recibe parámetros, debemos especificar el conjunto de valores para los que se devolverá el resultado que estamos definiendo. Por ejemplo.

@Test
public void testAddTwo(){		
	when(dependency.addTwo(1)).thenReturn(5);

	assertEquals(5, dependency.addTwo(1));	
	assertEquals(0, dependency.addTwo(27));
}

En el anterior test indicamos que cuando se llame a addTwo con el valor 1 se devuelva 5. Para el resto de valores, dependency se comporta como un dummy y devuelve 0, por ser el tipo de retorno un entero. Si la respuesta fuera un objeto, sería null.

Los parámetros de entrada se pueden definir con gran flexibilidad gracias a los métodos de ArgumentMatchers, por ejemplo:

@Test
public void testAddTwoAny(){		
	when(dependency.addTwo(anyInt())).thenReturn(0);

	assertEquals(0, dependency.addTwo(3));		
	assertEquals(0, dependency.addTwo(80));
}

¿No es suficiente? Podemos implementar el método con thenAnswer. En el siguiente ejemplo se define un comportamiento para addTwo en una clase anónima.

@Test
public void testAnswer() {		
when(dependency.addTwo(anyInt())).thenAnswer(new Answer<Integer>() {

		public Integer answer(InvocationOnMock invocation) throws Throwable {
			int arg = (Integer) invocation.getArguments()[0];
			return arg + 20;
		}
	});

assertEquals(30, dependency.addTwo(10));		
}

También es posible usar una lambda.

@Test
public void testAnswer() {		
    when(dependency.addTwo(anyInt())).thenAnswer(
        (Answer<Integer>) invocation -> {
	        int arg = (Integer) invocation.getArguments()[0];
		return arg + 20;
	});

    assertEquals(30, dependency.addTwo(10));		
}

Los ejemplos anteriores no nos sirven de referencia si un método no devuelve nada.

public void printMessage(String msg) {
    System.out.println(msg);
}

En estos casos, el patrón when…then se “invierte” y primero indicamos la acción asociada al when con métodos equivalentes a los que hemos visto (empiezan por do en vez de then).

@Test
public void testVoidDoAnswer() {
    String msg = "hello";

    doAnswer(invocation -> {
        Object arg0 = invocation.getArgument(0);
        assertEquals(msg, arg0);
        return null;
    }).when(dependency).printMessage(anyString());

    dependency.printMessage(msg);
}

@Test(expected = IllegalArgumentException.class)
public void testVoidException() {
    doThrow(IllegalArgumentException.class)
         		.when(dependency).printMessage(anyString());
    dependency.printMessage("");
}

Los test anteriores nos han permitido ver el funcionamiento básico de Mockito, pero si el lector se fija con atención no tienen sentido porque estamos “mockeando” el código que queremos probar. En una aplicación real, los dobles de test los haremos para las dependencias de la clase que estamos probando. Un ejemplo más realista es el siguiente en el que validamos Dependency con un stub de SubDependency.

package com.danielme.blog.testdouble;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.when;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

public class DependencyMockTest2 {

	@Mock
	private SubDependency subDependency;
	private Dependency dependency;

	@Before
	public void setupMock() {
		MockitoAnnotations.initMocks(this);
		dependency = new Dependency(subDependency);	
	}	

	@Test
	public void testSubdependency() {
		when(subDependency.getClassName()).thenReturn("hi there 2");	

		assertEquals("hi there 2", dependency.getSubdepedencyClassName());
	}	
	
}

Hemos construido el objeto a probar proporcionándole un @Mock. El código se puede simplificar con la anotación @InjectMocks.

package com.danielme.blog.testdouble;

import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import static org.mockito.Mockito.*;

import com.danielme.blog.testdouble.SubDependency;

public class DependencyMockTest2 {

	@Mock
	private SubDependency subDependency;
	@InjectMocks
	private Dependency dependency;

	@Before
	public void setupMock() {
		MockitoAnnotations.initMocks(this);		
	}	

	@Test
	public void testSubdependency() {
		when(subDependency.getClassName()).thenReturn("hi there 2");	
	
		assertEquals("hi there 2", dependency.getSubdepedencyClassName());
	}	
	
}

@Spy

Además de crear dobles de test con la anotación @Mock, podemos utilizar @Spy. Permite modificar directamente los métodos de un objeto real, de tal modo que aquellos que no cambiemos con when conservarán su funcionamiento original.

El uso de esta anotación es equivalente a @Mock. Si necesitamos aplicar un Spy de forma programática a un objeto usaremos el método estático spy. Veamos el siguiente ejemplo.

package com.danielme.blog.testdouble;

import static org.junit.Assert.*;
import static org.mockito.Mockito.*;

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

import com.danielme.blog.testdouble.SubDependency;

public class DependencySpyTest {

	private Dependency dependency;

	@Before
	public void setupMock() {
		dependency = spy(new Dependency(new SubDependency()));
	}

	@Test
	public void testOriginal() {
		assertEquals(7, dependency.addTwo(5));
	}

	@Test
	public void testSpy() {
		when(dependency.addTwo(Mockito.anyInt())).thenReturn(3);

		assertEquals(3, dependency.addTwo(27));
		assertEquals(SubDependency.class.getSimpleName(), dependency.getSubdepedencyClassName());
	}

}

En testOriginal se comprueba que el objeto dependency al que se le ha realizado un spy no es más que una instancia real del objeto, igual que aquella que obtenemos construyendo con new. Sin embargo, a un spy del mismo se le puede redefinir la implementación de sus métodos, tal y como vemos en testSpy con addTwo. Los métodos no modificados, como getSubdepedencyClassName(), hacen lo esperado.

verify

Otra funcionalidad interesante de Mockito es la posibilidad de analizar el uso de un objeto con el método verify. Indicaremos el objeto mock o spy y el número de veces (exacto, como mínimo, como máximo…) que se ha ejecutado uno de sus métodos. Si se desea, se puede filtrar según los parámetros de entrada.

El siguiente test sirve de ejemplo de uso de verify.

package com.danielme.blog.testdouble;

import static org.mockito.Mockito.*;

import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;

public class DependencyVerifyTest {

	@Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
	
	@Mock
	private Dependency dependency;	
	
	@Test
	public void testSimpleVerify() {	
		//nunca se ha ejecutado
		verify(dependency, never()).getClassNameUpperCase();
		
		dependency.getClassNameUpperCase();
		//exactamente una vez
		verify(dependency, times(1)).getClassNameUpperCase();
		//como mínimo una vez
		verify(dependency, atLeast(1)).getClassNameUpperCase();
		
		dependency.getClassNameUpperCase();
		//como máximo 2 veces
		verify(dependency, atMost(2)).getClassNameUpperCase();
	}
	
	@Test
	public void testParameters() {			
		dependency.addTwo(3);
		//una vez con el parámetro 3
		verify(dependency, times(1)).addTwo(3);
		
		dependency.addTwo(4);
		//dos veces con cualquier parámetro
		verify(dependency, times(2)).addTwo(anyInt());
	}	
	
}

Métodos estáticos

Una limitación histórica de Mockito es su incapacidad de “mockear” métodos estáticos. Y bien pensando, no debería ser una restricción importante porque si necesitamos esta funcionalidad quizás estemos haciendo un mal uso de los métodos estáticos, de los cuales se suele abusar -yo el primero-. Pero a veces no nos quedará más remedio que hacerlo, y la solución consiste en integrar Mockito con PowerMock.

Lo anterior fue cierto hasta la versión 3.4 (julio de 2020) con la introducción de los métodos mockStatic, cuyo uso, veremos, es un tanto peculiar.

package com.danielme.blog.testdouble;

public final class BoolUtils {

    private BoolUtils() {}

    public static boolean isTrue(Boolean bool) {
        return bool != null && bool;
    }
}

BoolUtils es la típica clase de utilidades. Aunque no tenga mucho sentido, vamos a mockear el método isTrue. Pero antes, debemos cambiar la dependencia de mockito-core a mockito-inline.

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-inline</artifactId>
    <version>${mockito.version}</version>
    <scope>test</scope>
</dependency>

Esta es nuestra clase de pruebas al completo.

package com.danielme.blog.testdouble;

import org.junit.Test;
import org.mockito.MockedStatic;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.mockStatic;

public class StaticMethodTest {

    @Test
    public void testStaticMocked() {
        try (MockedStatic<BoolUtils> utilsMocked = mockStatic(BoolUtils.class)) {
            utilsMocked.when(() -> BoolUtils.isTrue(anyBoolean())).thenReturn(false);

            assertFalse(BoolUtils.isTrue(true));
            assertFalse(BoolUtils.isTrue(false));
            assertFalse(BoolUtils.isTrue(null));
       }
    assertTrue(BoolUtils.isTrue(true));
   }
    
}

El mock de Mockito para la clase con el método estático se crea en un bloque try-with-resources y solo es válido dentro del mismo, tal y como verifica la aserción de la linea 21. Los métodos (isTrue) se modifican sobre la clase BoolUtils con una lambda dentro de un when aplicado, esta vez sí, al objeto mockeado (utilsMocked).

Integraciones

Si bien Mockito es independiente de cualquier librería de testing, echemos un rápido vistazo a su integración con otras herramientas.

JUnit 4. Contamos con un runner para instanciar los atributos marcados con @Mock y @Spy que nos evita hacerlo a mano en un método @Before.

@RunWith(MockitoJUnitRunner.class)
public class DependencyMockTest {

Debido a que solo es posible aplicar un runner a una clase, lo cual supone una limitación, es preferible recurrir a la regla que ya hemos visto.

JUnit 5. El runner anterior encuentra su equivalente en la extensión MockitoExtension, disponible en la siguiente dependencia.

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>3.10.0</version>
    <scope>test</scope>
</dependency>
@ExtendWith(MockitoExtension.class)
public class JupiterTest {

Como novedad, podemos usar parámetros en vez de atributos.

@Test
public void hasLocalMockInThisTest(@Mock Dependency dependency) {
   ...
}

El runner y la extensión lanzan una excepción si encuentran un método “mockeado” que nunca se invoca.

org.mockito.exceptions.misusing.UnnecessaryStubbingException: 
Unnecessary stubbings detected in test class: DependencyMockTest
Clean & maintainable test code requires zero unnecessary code.
Following stubbings are unnecessary (click to navigate to relevant line of code):
  1. -> at com.danielme.blog.testdouble.DependencyMockTest.testVoidDoAnswer(DependencyMockTest.java:86)
Please remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class.

	at org.mockito.internal.runners.StrictRunner.run(StrictRunner.java:53)

Si no se trata de un error y es algo que estamos haciendo de forma intencionada, tenemos que usar la siguiente versión alternativa del runner.

@RunWith(MockitoJUnitRunner.Silent.class)

O bien configurar la extensión.

@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)

Usar Mockito en Spring Boot, ya sea con JUnit 4 o JUnit 5, es una delicia porque las anotaciones @MockBean y @SpyBean inyectan dependencias en la clase de las pruebas, y además esos mocks se inyectan de forma transparente donde correspondan. Remito al lector al tutorial correspondiente.

Proyecto de ejemplo

El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

Otros tutoriales sobre testing

Testing con JUnit 4

Testing Spring con JUnit 4

Introducción al testing automatizado

JUnit 5

Testing en WildFly con Arquillian

Testing en Spring Boot con JUnit 4\5. Mockito, MockMvc, REST Assured, bases de datos embebidas.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .