Pruebas unitarias parte 3: Introducción a Spying con Mockito


En el post anterior hablamos sobre Pruebas unitarias parte 2: JUnit y Mockito primeros pasos, el siguiente paso será hablar sobre Spying.

¿Qué es un Spy y cómo es diferente a un Mock?

Un Mock reemplaza por completo la clase base y solo devuelve valores por defecto o valores definidos por nosotros, mientras que un Spy mantendrá los objetos originales y solo reemplazará algunos métodos, a esto se le conoce como partial mocking.

Veamos el siguiente ejemplo:

@Mock
private ArrayList mockArrayList;

@Test
public void mockTest() {
	mockArrayList.add("Object 1");
	mockArrayList.add("Object 2");
	mockArrayList.add("Object 3");
	assertEquals(0, mockArrayList.size());
}

En el ejemplo anterior se crea un mock de la clase ArrayList,  como se puede observar se ejecuta el método add 3 veces pero al realizar la validación assertEquals(0, mockArrayList.size()); esta devuelve verdadero y el test es exitoso.

La pregunta natural es ¿Por qué si se agregaron 3 objetos a la lista, al ejecutar el método mockArrayList.size() devuelve 0? La respuesta es simple, el objeto mock que creamos no contiene una implementación real de los métodos add() o size(), si no que solo su definición que será utilizada para pruebas.

En el siguiente ejemplo se mostrarán las ventajas de utilizar Spying:

@Spy
private ArrayList spyArrayList;

@Test
public void spyTest() {
	spyArrayList.add("Object 1");
	spyArrayList.add("Object 2");
	spyArrayList.add("Object 3");

	assertEquals(3, spyArrayList.size());

	when(spyArrayList.size()).thenReturn(20);
	assertEquals(20, spyArrayList.size());
}

Como se puede ver el código es muy similar al anterior, la diferencia es que estamos utilizando un Spy en lugar de un Mock, con esto crearemos un partial mock,este nos permitirá mantener el comportamiento original del objeto y solo sobre escribir algunos comportamientos, analicemos el código anterior:

  1. Se agrega el objeto Object 1 a la lista
  2. Se agrega el objeto Object 2 a la lista
  3. Se agrega el objeto Object 3 a la lista
  4. Se valida si el tamaño de la lista es 3, al estar utilizando un Spy en lugar de un Mock el resultado será verdadero así que el test sera exitoso
  5. Se sobre escribe el comportamiento del método size definiendo que la próxima vez que se ejecute el valor que devolverá será 20.
  6. Se valida si el tamaño de la lista es 20, como sobre escribimos el comportamiento del método size para que la próxima ejecución devuelva 20 el test será exitoso.

 

Como conclusión, se puede observar que es posible crear partial mocks utilizando Spying con mockito, el como usarlos en sus aplicaciones depende de ustedes.

Síguenos en nuestras redes sociales https://twitter.com/geeks_mx y https://www.facebook.com/geeksJavaMexico/ para recibir los siguientes posts que hablarán de temas más avanzados sobre Testing, JUnit y Mockito.

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com

Pruebas unitarias parte 2: JUnit y Mockito primeros pasos


Cuando hablamos de unit tests debemos hablar de “Mocking“, este es uno de los skills principales que debemos tener a la hora de hacer tests en nuestras aplicaciones.

Al momento de desarrollar aplicaciones las dividimos en capas, web, negocio y datos, entonces al escribir tests unitarios en una capa, no queremos preocuparnos por otra, así que la simulamos, a este proceso se le conoce como Mocking.

Test unitarios

Un test unitario debe probar un componente aislado de nuestra aplicación, los errores o efectos secundarios de otros componentes deben ser eliminados. La pregunta normal sería ¿Cómo aislar mi componente de sus dependencias para así probarlo? la respuesta es a través del uso de “test doubles“, estos se clasifican del siguiente modo:

  • Dummy objects : Este tipo de objetos son asignados al componente pero nunca se llaman y por lo general están vacíos.
  • Fake objects: Este tipo de objetos tienen implementaciones funcionales pero simplificadas, normalmente utilizan datos que no vienen de la base de datos principal, sino que se tienen en cache u otra fuente más simple.
  • Stub classes : Es una implementación parcial de una interfaz o clase con el propósito de utilizarla durante el test, normalmente solo cuentan con los métodos implementados que serán utilizados durante el test.
  • Mock objects: Es una implementación Dummy de una interfaz o clase en la cual se define la salida que tendrá la llamada de un método.

Estos “test doubles” aseguran que las pruebas se ejecuten solo sobre el componente que deseamos probar asegurando que no se tendrá ningún efecto secundario de ninguna otra clase.

Uso de Mocks

Es posible crear mocks manualmente, pero ya existen frameworks que pueden hacerlo por nosotros, estos permiten crear objetos mock en tiempo de ejecución y definir su comportamiento.

El ejemplo clásico de un objeto mock es un proveedor de datos, cuando se ejecuta la aplicación el componente se conectará a una base de datos y proveerá datos reales, pero cuando se ejecuta un test unitario lo que buscamos es aislarlo y para esto necesitamos un objeto mock que simulará la fuente de datos, esto asegurará que las condiciones de prueba sean siempre las mismas.

Un punto importante que se debe considerar es diseñar nuestros componentes para que sean probados fácilmente, si quieres saber más sobre esto te recomendamos el post Pruebas Unitarias Parte 1: ¿Cómo escribir código “Testeable”?.

Caso práctico con Mockito

Una vez que entendemos los conceptos básicos el siguiente paso es crear un ejemplo práctico, para hacerlo utilizaremos el framework de Mockito.

Configuración

Para incluir el soporte de Mockito en nuestra aplicación es necesario definir las siguientes dependencias:

Maven dependencies

Como se puede ver se incluye lo siguiente al proyecto:

  • Dependencia de Mockito : Brinda soporte para la creación de mocks
  • Dependencia de JUnit : Brinda soporte para la creación de tests unitarios
  • Maven compiler plugin : Define la versión de Java a utilizar

Ejemplo a realizar

En el ejemplo a realizar se crearán dos interfaces CalculatorService y DataService, con los siguientes propósitos:

  • DataService : Contendrá el método int[] getListOfNumbers(); que devolverá una lista de números de una fuente de datos.
  • CalculatorService : Contendrá el método double calculateAverage(); que calculará el promedio de la lista de números devuelta por el servicio DataService.

A demás se creará un test unitario que ayudará a probar el CalculatorService.

DataService.java

/**
 * @author raidentrance
 *
 */
public interface DataService {
	int[] getListOfNumbers();
}

CalculatorService.java

/**
 * @author raidentrance
 *
 */
public interface CalculatorService {
	double calculateAverage();
}

Una vez definidas las interfaces veamos la implementación de CalculatorService.
CalculatorServiceImpl.java


import com.raidentrance.services.CalculatorService;
import com.raidentrance.services.DataService;

/**
 * @author raidentrance
 *
 */
public class CalculatorServiceImpl implements CalculatorService {
	private DataService dataService;

	@Override
	public double calculateAverage() {
		int[] numbers = dataService.getListOfNumbers();
		double avg = 0;
		for (int i : numbers) {
			avg += i;
		}
		return (numbers.length > 0) ? avg / numbers.length : 0;
	}

	public void setDataService(DataService dataService) {
		this.dataService = dataService;
	}
}

En este caso no escribiremos implementación del DataService ya que haremos un mock del mismo para nuestras pruebas.

Probando nuestro componente

Una vez escritas las implementaciones de nuestros servicios el siguiente punto es probarlas, para esto crearemos un test unitario junto con mocks que permitan simular diferentes respuestas basadas en los tests, veamos el test de ejemplo:

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

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

import com.raidentrance.services.DataService;
import com.raidentrance.services.impl.CalculatorServiceImpl;

/**
 * @author raidentrance
 *
 */
@RunWith(MockitoJUnitRunner.class)
public class CalculatorServiceTest {
	@InjectMocks
	private CalculatorServiceImpl calculatorService;

	@Mock
	private DataService dataService;

	@Test
	public void testCalculateAvg_simpleInput() {
		when(dataService.getListOfNumbers()).thenReturn(new int[] { 1, 2, 3, 4, 5 });
		assertEquals(3.0, calculatorService.calculateAverage(), .01);
	}

	@Test
	public void testCalculateAvg_emptyInput() {
		when(dataService.getListOfNumbers()).thenReturn(new int[] {});
		assertEquals(0.0, calculatorService.calculateAverage(), .01);
	}

	@Test
	public void testCalculateAvg_singleInput() {
		when(dataService.getListOfNumbers()).thenReturn(new int[] { 1 });
		assertEquals(1.0, calculatorService.calculateAverage(), .01);
	}
}

Del código anterior podemos analizar lo siguiente:

  • @RunWith(MockitoJUnitRunner.class) : Se utiliza para definir que se utilizará el Runner de Mockito para ejecutar nuestras pruebas.
  • @Mock : Se utiliza para informar a Mockito que un objeto mock será inyectado en la referencia dataService, como se puede ver no es necesario escribir la clase implementación, Mockito lo hace por nosotros.

  • @InjectMocks : Se utiliza para informar a Mockito que los mocks serán inyectados en el servicio definido, es necesario que se cuente con métodos setters en los que se coloca.

  • when(dataService.getListOfNumbers()).thenReturn(new int[] { 1, 2, 3, 4, 5 }) : Define que cuando se ejecute el método getListOfNumbers se devolverá el resultado new int[] { 1, 2, 3, 4, 5 }.

Con lo anterior seremos capaces de probar nuestros componentes de forma aislada asegurando que las condiciones de nuestros tests siempre serán las mismas.

Síguenos en nuestras redes sociales https://twitter.com/geeks_mx y https://www.facebook.com/geeksJavaMexico/ para recibir los siguientes posts que hablarán de temas más avanzados sobre Testing, JUnit y Mockito.

Puedes encontrar el código completo en el siguiente enlace https://github.com/raidentrance/mockito-example.

Autor: Alejandro Agapito Bautista

Twitter: @raidentrance

Contacto:raidentrance@gmail.com