Contenido

Pruebas unitarias: Mocks y Stubs

Cuando estamos desarrollando, es necesario probar lo que estamos haciendo. En ocasiones esto puede resultar muy difícil (y más aún cuando no se ha hecho TDD), o muy lento.

En este artículo trataré de dar una introducción a la utilización de MOCKs y STUBs en estos casos.

En los ejemplos usaré Java y la librería mockito .

Voy a comenzar por plantear unas premisas: Las pruebas deben ser unitarias, rápidas e inocuas (no alteran el estado inicial).

Java

El problema

Como ejemplo, supongamos un programa que va a escribir en un archivo. Esto es un problema en sí mismo: Si el archivo existe, no podemos eliminarlo después, ya que cambiaría el estado inicial. Si el archivo no existe podemos crearlo y eliminarlo, pero puede que la prueba no sea tan rápida como nos gustaría.

Aún así, hagamos el método inicial, que tan sólo va a escribir “a sentence” en un archivo:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void writeASentence(FileWriter writer) throws IOException {
  writer.write("una cadena");
  writer.flush();
}

@Test
public void testWriteASentence() throws IOException {
  File file = new File("output");
  file.createNewFile();
  file.deleteOnExit();

  FileWriter writer = new FileWriter (file);

  writeASentence (writer);

  writer.close();

  file.close();
}

Aquí tenemos la función que queremos probar y un primer test.

Problemas de este test:

  • No prueba nada.
  • Si el fichero existe, lo borrará.
  • Si se invoca muchas veces, será lento.
  • Sin querer, estamos probando parte de la funcionalidad de “File”.

Así que necesitamos algo más potente. Comprobar el fichero contiene lo que se deseaba escribir es una operación que puede ser terriblemente compleja en comparación con el propio método.

Más sencillo

Para ello podemos crearnos un objeto “mock”. Mockito puede ayudarnos a ello:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import static org.mockito.Mockito.*;

public void writeASentence(FileWriter writer) throws IOException {
  writer.write("una cadena");
  writer.flush();
}

@Test
public void testWriteASentence() throws IOException {
  FileWriter writer = mock(FileWriter.class);

  writeASentence (writer);
}

Este código es exacto al anterior. Sigue sin probar nada, pero ya no modifica ni requiere ningún archivo.

La solución

Realmente sólo queremos comprobar que se llamó a los métodos “write” y “flush” del objeto que le paso por parámetro, y estaría bien comprobar que los argumentos de estos dos métodos eran correctos. Para ello:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import static org.mockito.Mockito.*;

public void writeASentence(FileWriter writer) throws IOException {
  writer.write("una cadena");
  writer.flush();
}

@Test
public void testWriteASentence() throws IOException {
  FileWriter writer = mock(FileWriter.class);

  writeASentence (writer);

  verify (writer).write("una cadena");
  verify (writer).flush();
}

Y ya se está comprobando que nuestro método realiza las operaciones deseadas.

Solución con acceso a datos

¿Qué ocurre si lo que deseamos es obtener datos? Podemos pensar que, en ese caso, no nos queda más remedio que utilizar el objeto real. Sin embargo, para eso están los STUBs. Mockito nos permite mezclar ambos de manera sencilla.

Vamos a añadir a nuestro ejemplo una absurda función que comprueba que un fichero existe:

1
2
3
4
5
6
7
import static org.mockito.Mockito.*;

public void assertFileExistence (File file) throws Exception {
   if ( file.exists() )
      return;
   throw new Exception ("error");
}

Cualquier intento que hagamos de probar la función sin usar un “File” real dará problemas al llamar a la función. Engañémosla:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import static org.mockito.Mockito.*;

public void assertFileExistence (File file) throws Exception {
   if ( file.exists() )
      return;
   throw new Exception ("error");
}

@Test
public void testAssertFileExistenceWhenItExists () throws Exception
{
   File file = mock (File.class);

   when ( file.exists() ).thenReturn (true);

   assertFileExistence (file)

   verify (file).exists();
}

@Test (expected=Exception.class)
public void testAssertFileExistenceWhenItExists () throws Exception
{
   File file = mock (File.class);

   when ( file.exists() ).thenReturn (false);

   assertFileExistence (file)
}

Y ahora sí, tenemos una función atómica, que se está probando con exclusividad, perfectamente definida y que no altera el estado de nada.

¿Algún problema con esto?

Pues mucho me temo que sí. No podía ser perfecto. Por desgracia necesitamos un gran conocimiento de lo que ocurre dentro. Si son pruebas unitarias, tendremos ese conocimiento, pero casi cualquier cambio en el método a probar alterará la prueba. La utilización de más de un Mock/Stub puede suponer una alteración en el comportamiento real de lo que estemos probando, y puede requerir numerosos casos de interacción entre ellos.

Entonces… ¿Para qué sirven?

Cuando queremos modificar una base de datos resulta casi imposible ser inocuo. Al transmitir datos por red o al escribir en fichero también puede ser un problema. En estos caso puede ser bastante interesante recurrir a Mocks o Stubs.

Además, si necesitamos muchos Mocks y Stubs para realizar una única prueba es señal de que algo está mal en el método a probar.

Recordad que un método, una clase, debería hacer una única cosa. Con este principio, ¡todo es tan sencillo de probar!

Notas y más información

Tened en cuenta que los Mocks no son Stubs.

También tenéis ayuda de en la web de mockito, o podéis decantaros por easymock.

Otros lenguajes también tienen sus mocks, como Python, Ruby, PHP, … (si conocéis otros mejores o bien más frameworks, podéis indicármelo y los añadiré).