Test Doubles con Mockito

mockito

Al testear cierto comportamiento de un objeto nos encontraremos habitualmente con que ese objeto posee una o varias dependencias de otros objetos en los que se apoya y que, por tanto, condicionan el comportamiento del código que queremos testear. A veces incluso esas dependencias nos impiden la realización de los tests ya que pueden conectarse a servicios, bases de datos, etc, sobre los que no tenemos el control y no podemos configurar para hace nuestros tests.

Un test double o doble de test es un objeto utilizado en los tests que reemplaza a uno equivalente en la aplicación real y cuyo comportamiento podemos modelar a nuestra conveniencia en cada momento para que nos ayude a testear como queramos los objetos que dependen de él. De este modo se aísla el código que estamos testeando, algo fundamental en el que caso de que estemos haciendo testing unitario, y podemos probarlo a fondo, simulando si hiciera falta múltiples comportamientos del test double (por ejemplo, devolver ciertos resultados, excepciones, etc, para comprobar que el código que utiliza el test double se comporta como debería en cada situación).

A los test doubles se les suele denominar genéricamente mocks (bocetos) pero, siendo estrictos, se suelen clasificar en cinco tipos:

  • Dummy: objetos que necesitamos para ejecutar el test pero que no hacen nada, o da lo mismo lo que hagan porque no afectan al test.
  • Stub: es como un dummy pues sus métodos no hacen nada, pero devuelven cierto valor que necesitamos para ejecutar nuestro test con respecto a ciertas condiciones.
  • Spy: permite “espiar” el uso que se hace del propio objeto, por ejemplo el número de veces que se ejecuta un método, los argumentos que se le van pasando (se les puede hacer un assert), etc.
  • Mock: es un stub en el que sus métodos sí implementan un comportamiento pues esperan recibir unos valores y en función de ellos devuelve una respuesta.
  • Fake: es un objeto “correcta y completamente” implementado y que es totalmente equivalente al objeto real al que simula pero falsea algún comportamiento que no puede ser aplicado en los tests. Ejemplo típico: 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.

En Java es relativamente sencillo implementar los test doubles que necesitemos si seguimos los principios SOLID, pero también disponemos de herramientas que simplifican esta tarea porque pueden modificar de forma dinámica el comportamiento de métodos en cada momento sin necesidad no sólo de tener que implementar el test double completo, sino que ni siquiera es necesario implementar un nuevo objeto. En este tutorial veremos el uso básico de Mockito, un framework para test doubles en Java muy popular y fácil de utilizar y que además cuenta con una versión específica para Android.

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 sólo tenemos que incluir una dependencia con el scope test.

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

Salvo los import que nos vayan haciendo falta no es necesario utilizar ningún runner o anotación especial a nivel de clase.

Empecemos construyendo una clase de tests en el que modifiquemos directamnete el comportamiento de los métodos de la clase 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.
    	@Before
    	public void setupMock() {
    		MockitoAnnotations.initMocks(this);		
    	}
    

    Si queremos instaciar de forma específica un objeto con Mockito podemos hacerlo de forma estática con mock

        @Before
    	public void setupMock() {
    		dependency = mock(Dependency.class);	
    	}
    
  3. Otra opción es hacer que mockito instancie de forma automática los @Mock utilizando una regla.
    @Rule public MockitoRule mockitoRule = MockitoJUnit.rule();
    

Independientemente de la opción elegida, ya tenemos el atributo dependency con una instancia mock listo para ser utilizado en los métodos de tests. Si inspeccionamos este objeto con el debugger veremos el objeto que realmente es:

Se trata de un dummy y todas las llamadas a sus métodos devuelven null tal y como podemos comprobar con el siguiente test.

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

Para definir el comportamiento de un método del objeto mock usamos el método estático when para indicar el método a “implementar”. Con un método del objeto que devuelve la llamada a when se indica el resultado que devuelve el mock para ese método. Veamos un ejemplo:

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

En este test estamos diciéndole a Mockito que la llamada al método getClassName() devuelva siempre la cadena “hi there”, con lo que ahora nuestro mock pasa de ser un dummy a un stub. Sólo hemos necesitado una línea de código dentro del test.

También podríamos forzar que se lance 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 cuyo comportamiento estamos definiendo recibe parámetros, debemos especificar el valor\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 al método addTwo con el valor 1 se devuelva 5. Para el resto de valores, dependency se comporta como un dummy y devuelve null (en este caso como devuelve un int retorna 0, su valor por defecto).

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));
}

Para implementar el método y así definir un auténtico mock usamos thenAnswer. En el siguiente ejemplo se implementa 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));		
}

Los test anteriores nos han permitido ver el funcionamiento básico de Mockito, pero en el mundo real los test doubles los haremos para las dependencias de la clase que estamos testeando. Un ejemplo más realista es el siguiente en el que testeamos el funcionamiento de la clase Dependency pero con un stub deSubdependency.

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());
	}	
	
}

Esta clase se puede simplificar utilizando 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 test doubles con la anotación @Mock, podemos utilizar la anotación @Spy que permite modificar directamente el comportamiento del objeto real en lugar de crear un dummy al que vamos definiendo el comportamiento de sus métodos si fuera necesario para crear stubs y mocks. Por tanto, los métodos de un @Spy que no modifiquemos con Mockito conservarán su comportamiento 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 puede comprobar que el objeto dependency al que se le ha realizado un spy no es más que una instancia real del objeto, igual que el que obtenemos construyendo haciendo un new. Sin embargo al spy se le pueden redefinir el comportamiento de sus métodos, tal y como vemos en testSpy con el método addTwo. Los métodos no modificados, como getSubdepedencyClassName(), conservan su comportamiento original.

verify

En la introducción vimos que los test doubles de tipo Spy, y opcionalmente los de tipo Mock, permiten trazar el uso del objeto. Mockito ofrece esta funcionalidad con el método verify al que indicaremos el objeto “mockeado” con Mockito y el número de veces (exacto, como mínimo, como máximo…) que se ha ejecutado un método de ese objeto mock teniéndose en cuenta si se desea los posibles valores de 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());
	}	
	
}

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

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. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: