Testing de Hadoop con MRUnit

0
6507

Testing de Hadoop con MRUnit

 

0. Índice de contenidos.

1. Introducción.

Para hacer test unitarios de algoritmos Map Reduce disponemos de la librería Apache MRUnit, una librería orientada a probar algoritmos Map Reduce de Hadoop que se integra con JUnit. Para asegurarnos que los cambios futuros en el software no rompen nada de lo que ya funciona, debemos hacer test automáticos de manera que el ciclo de vida asegure que se ponen en verde la batería de tests con cada cambio nuevo que se introduzca.

Por ello no debería ser una recomendación sino una obligación dejar probado con tests unitarios que nuestro código hace lo que debe ya que si mañana alguien o nosotros mismos metemos cualquier cambio en el algoritmo, nos aseguremos que todo sigue funcionando perfectamente.

En este tutorial vamos a hacer test unitarios y de integración para probar cómo funciona MRUnit.

Puedes descargarte el código del tutorial desde mi repositorio de github pinchando aquí.

2. Entorno.

El tutorial se ha realizado con el siguiente entorno:

  • Ubuntu 12.04 64 bits
  • Oracle Java SDK 1.6.0_27
  • Apache Hadoop 2.2.0
  • MRUnit 1.0.0
  • Apache Maven 3.1.1

3. Configuración del proyecto

Partiremos del proyecto que creamos en este tutorial. Teníamos un algoritmo MapReduce que procesaba un fichero con datos climatológicos donde el resultado era la agrupación de los datos históricos por uno de los índices de polución por cada provincia de Castilla y León. Ahora vamos a hacer tests con MRUnit para asegurar que nuestro algoritmo funciona correctamente. El desarrollo lógico habría sido hacer el test junto al código y no después aunque nunca es tarde 😛

Aunque lógicamente podemos testear las clases con JUnit y mockear las clases de Hadoop con Mockito por ejemplo, MRUnit nos facilita mucho el trabajo ya que nos proporciona drivers específicos para el mapper y el reducer. También dispone de un driver para hacer tests de integración y poder probar las clases en conjunto.

Para empezar añadimos al pom.xml las librerías de JUnit y MRUnit.

	
<dependencies>
    <dependency>
        <groupid>junit</groupid>
        <artifactid>junit</artifactid>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
 
    <dependency>
        <groupid>org.apache.mrunit</groupid>
        <artifactid>mrunit</artifactid>
        <version>1.0.0</version>
        <classifier>hadoop2</classifier>
        <scope>test</scope>
    </dependency>
</dependencies>

Importante!! Hay que indicarle en el classifier la versión de Hadoop que tenemos instalada.

4. Tests unitarios.

Para las pruebas del mapper, MRUnit nos proporciona la clase MapDriver. En nuestro test debemos crear una instancia de esta clase pasándole en el constructor el mapper que queremos probar. Para el reducer existe un driver similar.

En el test debemos configurar la entrada y salida del MapDriver con los métodos withInput indicando los datos de entrada al mapper y withOutput con la salida esperada. Lanzamos el test llamando al método runTest el cuál llamará al método map de nuestro mapper pasándole en la entrada los datos que metimos en el withInput. Si la salida del mapper no corresponde con lo que metimos en el método withOutput se lanzará una AssertionError haciendo fallar el test.

package com.autentia.tutoriales;

import java.io.IOException;
import java.util.ArrayList;

import org.apache.hadoop.io.DoubleWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mrunit.mapreduce.MapDriver;
import org.apache.hadoop.mrunit.mapreduce.MapReduceDriver;
import org.apache.hadoop.mrunit.mapreduce.ReduceDriver;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class AirQualityTest {

	private MapDriver<LongWritable, Text, Text, DoubleWritable> mapDriver;
	private ReduceDriver<Text, DoubleWritable, Text, Text> reduceDriver;
	private MapReduceDriver mapReduceDriver;

	private final AirQualityMapper mapper = new AirQualityMapper();
	private final AirQualityReducer reducer = new AirQualityReducer();

	@Before
	public void setUp() {
		mapDriver = new MapDriver(mapper);
		reduceDriver = new ReduceDriver(reducer);
		mapReduceDriver = new MapReduceDriver(mapper, reducer);
	}

	@Test
	public void shouldReturnCOValueForProvinceOnlyWhenValueIsANumber() throws IOException {
		mapDriver.withInput(new LongWritable(0), new Text("01/01/1997;1.2;12;33;63;56;;;;19;ÁVILA;ávila"));
		mapDriver.withOutput(new Text("ÁVILA"), new DoubleWritable(1.2));

		mapDriver.runTest();
	}

	@Test
	public void shouldReturnEmptyResultMapWhenValueIsNotNumber() throws IOException {
		mapDriver.withInput(new LongWritable(0), new Text("DIA;CO (mg/m3);NO (ug/m3);NO2 (ug/m3);O3 (ug/m3);PM10 (ug/m3);SH2 (ug/m3);PM25 (ug/m3);PST (ug/m3);SO2 (ug/m3);PROVINCIA;ESTACIóN"));

		Assert.assertTrue(mapDriver.run().isEmpty());
	}
}

Otra opción si queremos recuperar la salida del mapper es utilizar el método run() del mapDriver. Este método ejecutará la tarea Map y nos devolverá el resultado para que seamos nosotros quienes evaluemos el resultado. Los tests del reducer son muy similares.

...
	@Test
	public void shouldReturnTheAverageOfValuesByProvince() throws IOException {
		reduceDriver.withInput(new Text("ÁVILA"), new ArrayList<DoubleWritable>() {
			{
				add(new DoubleWritable(1.5));
				add(new DoubleWritable(1.4));
				add(new DoubleWritable(1.6));
			}
		});

		reduceDriver.withOutput(new Text("ÁVILA"), new Text("1.5"));

		reduceDriver.runTest();
	}

	@Test
	public void shouldReturnEmptyResultReduceWhenValuesListIsEmpty() throws IOException {
		reduceDriver.withInput(new Text("ÁVILA"), new ArrayList<DoubleWritable>());

		Assert.assertTrue(reduceDriver.run().isEmpty());
	}

5. Tests de integración.

Una vez probados el mapper y el reducer por separado vamos a hacer un test de integración que pruebe todo en conjunto. El funcionamiento es similar al anterior, creamos un mapReduceDriver configurando la entrada y el resultado esperado para la salida. Probaremos varios casos, con datos buenos y malos y varios tipos de salida.

	@Test
	public void shouldMapInputLinesAndReduceByCOAndProvince() throws IOException {
		mapReduceDriver
				.withInput(new LongWritable(1), new Text("01/01/1997;1.2;12;33;63;56;;;;19;ÁVILA;Ávila"))
				.withInput(new LongWritable(2), new Text("22/04/1997;1.1;45;49;75;No cumple el anexo IV...;;;;16;ÁVILA;Ávila"))

				.withInput(new LongWritable(3), new Text("24/12/2003;1.6;27;22;45;25;4;;;6;BURGOS;Miranda de Ebro 1"))
				.withInput(new LongWritable(4), new Text("25/12/2003;2.2;35;13;53;28;21;;;7;BURGOS;Miranda de Ebro 1"))
				.withInput(new LongWritable(5), new Text("26/12/2003;No cumple el anexo IV...;No cumple el anexo IV...;No cumple el anexo IV...;No cumple el anexo IV...;No cumple el anexo IV...;No cumple el anexo IV...;;;No cumple el anexo IV...;BURGOS;Miranda de Ebro 1"))
				.withInput(new LongWritable(6), new Text("27/12/2003; 0.1;18;15;45;30;24;;;7;BURGOS;Miranda de Ebro 1"))
				
				.withOutput(new Text("BURGOS"), new Text("1.3"))
				.withOutput(new Text("ÁVILA"), new Text("1.15"))
				
				.runTest();
	}

Si todo ha ido bien se pondrán en verde nuestros tests y lo mejor, que al quedar integrados en el ciclo de vida de nuestro proyecto nos aseguramos que todo seguirá funcionando.

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.autentia.tutoriales.AirQualityTest
18:06:53,514 DEBUG main util.Shell:326 - setsid exited with exit code 0
18:06:53,988 DEBUG main mrunit.MapDriverBase:296 - Mapping input (0, 01/01/1997;1.2;12;33;63;56;;;;19;ÁVILA;Ávila)
18:06:54,094 DEBUG main mrunit.TestDriver:651 - Matched expected output (ÁVILA, 1.2) at positions [0]
18:06:54,188 DEBUG main mapreduce.MapReduceDriver:252 - Starting map phase with mapper: com.autentia.tutoriales.AirQualityMapper@1a1a614
18:06:54,208 DEBUG main mapreduce.MapReduceDriver:264 - Starting reduce phase with reducer: com.autentia.tutoriales.AirQualityReducer@1b88d41
18:06:54,238 DEBUG main mapreduce.MapReduceDriver:220 - Reducing input (BURGOS, (1.6, 2.2, 0.1))
18:06:54,239 DEBUG main mapreduce.MapReduceDriver:220 - Reducing input (ÁVILA, (1.2, 1.1))
18:06:54,270 DEBUG main mrunit.TestDriver:651 - Matched expected output (ÁVILA, 1.15) at positions [1]
18:06:54,279 DEBUG main mrunit.TestDriver:651 - Matched expected output (BURGOS, 1.3) at positions [0]
18:06:54,458 DEBUG main mrunit.TestDriver:338 - Reducing input (ÁVILA, (1.5, 1.4, 1.6))
18:06:54,473 DEBUG main mrunit.TestDriver:651 - Matched expected output (ÁVILA, 1.5) at positions [0]
Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.84 sec

6. Conclusiones.

Como véis, hacer tests de algoritmos MapReduce es muy sencillo si tienes las herramientas adecuadas. Conociendo JUnit te resultará muy sencillo utilizar MRUnit ya que su funcionamiento es similar.

Puedes descargarte el código del tutorial desde mi repositorio de github pinchando aquí.

Espero que te haya sido de ayuda.

Un saludo.

Juan

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad