Antes de iniciar con este apartado, es conveniente aclarar que los estilos de pruebas unitarias clásico y burlón son conocidos de diferentes formas, lo que en muchas ocasiones puede confundir al lector o investigador de estos temas. El estilo clásico es muy conocido en la literatura como escuela de Chicago o Detroit y el estilo burlón como escuela de Londres o Mockist.
Como vimos anteriormente, TDD es una práctica de programación que consiste en escribir primero las pruebas (generalmente unitarias), después escribir el código fuente que pase la prueba satisfactoriamente y, por último, refactorizar el código escrito; lo que permite una mayor seguridad, robustez y mantenibilidad en el código. En la actualidad existen dos escuelas de TDD con tendencias distintas a realizarla, la primera, es la escuela de Chicago (estilo clásico) enfocada en el estado y la segunda, escuela de Londres (estilo mockist) basada en comportamiento o interacción. En el caso de JUnit, la librería vista anteriormente, está diseñada bajo el enfoque del estilo clásico, manejando el estado como medio de comprobación entre un valor esperado y uno generado.
El estilo clásico de TDD usa objetos reales si es posible y un doble cuando no se puede utilizar uno real; mientras que para el caso de mockist TDD, siempre se utilizará un doble para cualquier objeto.
Entre ambos existen unas diferencias.
|
Clásico TDD |
Mockist TDD |
|
Verifica el estado |
Verifica el comportamiento |
|
Su trabajo generalmente es con objetos reales |
Se prefiere el trabajo con objetos falsos |
|
Las pruebas tienden a ser más de grano grueso, acercándose a más pruebas de estilo de integración |
Las pruebas tienden a ser muy finas; pueden faltar integraciones |
|
Usa mocks para probar colaboraciones |
Usa mocks todo el tiempo |
test double es simplemente un tipo de objeto que sustituye a uno real cuando se va a escribir pruebas, siendo mucho más cooperativo con lo que queremos.
Importante: Como ya sabemos, una prueba unitaria es en pocas palabras, la acción de verificar el correcto funcionamiento de un componente de nuestra aplicación, sin embargo, para que esto ocurra debemos aislar el componente de sus dependencias., y la manera de hacerlo, es a través de test double.
Tipos de Doubles:
En próximos apartados, se ampliara más el concepto de dobles de prueba y se darán ejemplos de ellos para entender más su funcionamiento, pero primero, veremos un framework que nos ayudará a construir de manera sencilla nuestros test doubles, este framework es conocido como mockito.
Como métodos manejados por Mockito están:
|
Método |
Descripción |
|
Mock() |
Este método se usa para crear objetos simulados e una clase o interfaz, permitiendo cinco métodos mock() con argumentos diferentes. |
|
When() |
Se usa cuando se pretende devolver valores específicos cuando se llaman ciertos métodos |
|
Verify() |
Su uso se da cuando se desea comprobar si se llama a alguno de los métodos especificados, Usado en la parte inferior del código |
|
Spy() |
Método conocido como espía que simula parcialmente un objeto, este, anula los métodos específicos del objeto real |
|
Reset() |
Permite restablecer los mocks y rara vez es usado en pruebas |
|
verifyNoMoreInteractions() |
Es un método opcional que no es necesario usar en cada prueba. Su función es comprobar que cualquiera de los mocks tiene interacciones no verificadas. |
|
verifyZeroInteractions() |
Verifica que no se ha producido ninguna interacción en los mocks especificados. Al igual que el anterior método detecta las invocaciones producidas antes del método prueba, por ejemplo, en el setup() o @Before |
|
doThrow() |
Se usa en el código auxiliar de un método void para producir una excepción |
|
doCallRealMethod() |
Se usa cuando se desea llamar la implementación real de un método |
|
doAnswer() |
Se utiliza cuando queremos stub en un método void con un tipo de respuesta genérico |
|
doReturn() |
Es usado en raras ocasiones en las que el método when() no se puede usar. Sin embargo, para stubbing se sugiere usar when () por ser más seguro para el tipo de argumento además de ser más legible |
|
inOrder() |
Usado para crear objetos que verifica las simulaciones en un orden específico |
|
ignoreStubs() |
Es útil con los métodos verifyNoMoreInteractions() o verification inOrder(). Se usa para ignorar los métodos de código auxiliar de ciertos simulacros para la verificación |
|
times() |
Útil para comprobar el número exacto de invocaciones de un método |
|
never() |
Comprueba que la interacción no se realizó |
|
atLeastOnce() |
Se usa para verificar que la invocación al método se haya realizado al menos una vez |
|
atLeast() |
Necesita como parámetro, un número que indique la cantidad de veces que se debe invocar el método |
|
atMost() |
Requiere de un parámetro que indique el número máximo de invocaciones al método se permiten. |
|
calls() |
Sólo se puede utilizar con el método de verificación inOrder(). Permite una verificación no expansiva en orden |
|
only() |
Comprueba que el método especificado era el único método invocado |
|
validateMockitoUsage() |
Valida explícitamente el estado del marco de trabajo. El ejecutor integrado (MockitoJUnitRunner) y la regla (MockitoRule) llama al método validateMockitoUsage() después de cada método de prueba |
Al igual que JUnit, mockito también usa anotaciones, por lo cual es importante conocerlas.
Es de aclarar, que si se desea inyectar un simulacro o mock en un espía, se debe hacer manualmente a través de un constructor
Instalación
Como requisito principal para hacer uso del framework de mocking, debemos tener la versión 1.5 o superior del JDK (Java Developtmen Kit) en nuestra máquina. En nuestro caso haremos uso como ya lo hemos venido haciendo del IDE Eclipse y de JUnit4 como framework para pruebas unitarias de Java.
En el caso de usar maven debemos añadir la siguiente dependencia al pom.xml
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.2.4</version> </dependency>
En el caso de usar gradle añadimos la siguiente dependencia al gradle build file
repositories { jcenter() } dependencies { testImplementation 'org.mockito:mockito-core: 2.7.22'}
Para habilitar las anotaciones existen varias formas.
import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; @RunWith(MockitoJUnitRunner.class) public class AnnotationJUnitRunner{ }
La
segunda opción es a través de la programación invocando a
MockitoAnnotations.initMocks().
import org.mockito.MockitoAnnotations; public class AnnotationsInit{ @Before public void inicializaMocks() { MockitoAnnotations.initMocks(this); } }
La tercera y última opción es a través de MockitoJUnit.rule()
public class MockitoWithJUnitRuler{ @Rule public MockitoRule iniRule = MockitoJUnit.rule(); }
Como ya se definió anteriormente, las pruebas unitarias se centran en verificar que cada pequeño trozo de código (clase, función, método, componente, etc.) al que llamaremos de ahora en adelante SUT (Subject under Test – Objeto Bajo Prueba) que constituye el sistema funciona como debe ser.
Hay ocasiones en las que es posible que el SUT dependa de otra clase conocida como DOC (Depended-on Component), y es en estos casos, en los que se hace necesaria la incorporación de un Test Double o sustituto, con el fin de aislar su dependencia de ellos; esto permite probar escenarios que podrían ser difíciles de probar con los DOCs reales o que no están disponibles porque están en fases de desarrollo, este comportamiento es característico de las pruebas unitarias solitarias.
Un test double es entonces, un tipo de objeto que sustituye a los DOC para la ejecución de la prueba unitaria que nos permite escribir la prueba de la forma en la que nosotros queremos.
Para ilustrar mejor el concepto veamos el siguiente ejemplo:
Supongamos un sistema de gestión de pedidos, el cual funciona por la interacción de 4 clases:
El SUT es la clase Factura: Se quiere hacer una prueba unitaria de la clase Factura, que como se puede observar interactúa con las clases Cliente y LineaFactura, y además a través de LineaFactura interactúa con Producto, para realizar una prueba unitaria sólo a la pieza factura podemos utilizar dobles de prueba para sustituir las otras tres clases, consiguiendo de esta forma aislar la prueba de Factura de la prueba de Cliente, Producto y LineaFactura.
Esta es la misión de los dobles de prueba que suplantan a las piezas originales durante la ejecución de la prueba unitaria de Factura:
¿Por qué es importante hacer uso de dobles?
El uso de dobles evita que tengamos que añadir métodos exclusivos para realizar pruebas al objeto, algo que siempre debemos evitar, pues la prueba no debe verse afectada por temas ajenos al funcionamiento de la aplicación. Los test doubles permiten que se tenga mayor control en las especificaciones del comportamiento de un objeto, de la siguiente manera:
Ventajas de los dobles de test
Dummy:
Cuando hablamos del objeto Dummy, nos encontramos con que es un Test Double que en realidad no tiene implementación, pero que básicamente se utilizan para llenar argumentos de llamadas a métodos que son irrelevantes para la prueba.
Función:
Para trabajar con el objeto Dummy, se debe crear una instancia de algún objeto que pueda ser instanciado fácilmente y que no contenga dependencias, como paso siguiente pasamos la instancia como argumento del sujeto bajo prueba (SUT) y como en realidad no se utilizara dentro del sistema o sujeto bajo prueba no es necesario que exista alguna implementación sobre el objeto. Si por alguna razón se hace un llamado a alguno de los métodos del objeto simulado, la prueba debe arrojar un error ya que sería lo mismo que llamar un método inexistente.
package com.dummy.modelo; public class Producto { String nombre; int cantidad; public Producto (String nombre, int cantidad) { this.nombre = nombre; this.cantidad =cantidad; } }
package com.dummy.test; import static org.junit.jupiter.api.Assertions.*; import java.util.ArrayList; import java.util.List; import org.junit.jupiter.api.Test; import com.dummy.modelo.Producto; class ProductoTest { @Test void testListaCarrito() { List<Producto> listaCarrito = new ArrayList<>(); //creamos los productos Producto prodDummy1 = new Producto("Arroz", 0); Producto prodDummy2 = new Producto("Hola", 1); //añadimos a los productos a la lista listaCarrito.add(prodDummy1); listaCarrito.add(prodDummy2); //assert assertEquals(2, listaCarrito.size()); } }
Fake
¿Cómo funciona?
Al utilizar este tipo de test double, lo primero que se debe hacer es construir una implementación con la misma funcionalidad del objeto real del cual depende el objeto bajo prueba, como paso siguiente se debe indicar al SUT que va a ser uso de la implementación en lugar del objeto real, La implementación necesita proporcionar los servicios equivalentes al sujeto bajo prueba para que este permanezca inconsciente de que no está usando el objeto real.
¿Cuándo Usarlo?
Usamos un objeto Fake para simular o reemplazar la funcionalidad de un objeto real en una prueba que no se centra en la verificación de entradas y salidas indirectas del SUT, una de las razones más comunes para usar este tipo de test doubles es que por alguna razón el objeto real no se encuentra disponibles, es muy lento o no se puede utilizar en el ambiente de prueba debido a efectos secundarios que se pueden presentar.
Ejemplo:
Para nuestro ejemplo, crearemos un fake de una calculadora que nos permitirá calcular un promedio, una raíz cuadrada de acuerdo a datos proporcionados y finalmente, se podrá realizar el cálculo de ecuaciones de segundo grado a través de la aplicación de formula general o formula del estudiante, a continuación se presenta la formula general.

Lo primero que debemos hacer, es crear las interfaces (abstractas) necesarias, en este caso ServicioDeDatos y ServicioCalculadora. La interface ServicioDeDatos, proporcionará una lista de números para calcular el promedio, un número único para calcular la raíz cuadrada y la lista de coeficientes para calcular los valores de la ecuación de segundo grado.
//Interface que devuelve números de una fuente de datos public interface ServicioDeDatos { int [] obtenerListaNumeros(); int obtenerNumeroRaiz(); double [] obtenerCoeficientes(); }
La interface ServicioCalculadora contendrá los métodos calcularPromedio(), calcularRaizCuadrada() y calcularFormulaGeneral() que serán invocadas de acuerdo a los datos proporcionados por la interface ServicioDeDatos
//Interface que contiene el llamado a los métodos para calcular diferentes funcionalidades de acuerdo a lo //mandado por la interface ServicioDeDatos public interface ServicioCalculadora { double calcularPromedio(); double calcularRaizCuadrada(); double [] calcularFormulaGeneral(); }
Siguiendo ese orden,
crearemos una clase llamada ServicioCalculadoraImplementacion, la cual
implementará ServicioCalculadora que permitirá crear todos los métodos
definidos en dicha interface
import interfaceSD.ServicioCalculadora; import interfaceSD.ServicioDeDatos; //Se hace la implementación de los servicios public class ServicioCalculadoraImplementacion implements ServicioCalculadora { private ServicioDeDatos servicioDatos; public void setServicioDeDatos(ServicioDeDatos servicioDatos) { this.servicioDatos=servicioDatos; } public double calcularPromedio() { int [] numeros = servicioDatos.obtenerListaNumeros(); double avg=0; for(int i : numeros) { avg+=i; } return (numeros.length>0) ? avg/numeros.length : 0; } public double calcularRaizCuadrada() { int numero = servicioDatos.obtenerNumeroRaiz(); double raizCuadrada = Math.sqrt(numero); return raizCuadrada; } public double [] calcularFormulaGeneral() { double [] coeficientes = servicioDatos.obtenerCoeficientes(); double coeficienteA= coeficientes[0]; double coeficienteB= coeficientes[1]; double coeficienteC= coeficientes[2]; double x1, x2 = 0; double primerCalculo= Math.pow(coeficienteB, 2) - 4*coeficienteA*coeficienteC; if(primerCalculo<0) { double [] resultado = {0}; return resultado; }else { x1= ((-(coeficienteB))+Math.sqrt(primerCalculo))/(2*coeficienteA); x2= ((-(coeficienteB))-Math.sqrt(primerCalculo))/(2*coeficienteA); double [] resultado = {x1, x2}; return resultado; } } }
Con lo anterior tendremos un doble de prueba de tipo fake, el cual no necesitara ser configurado en relación a sus respuestas o expectativas.
Un objeto mock o simulado es un objeto que reemplaza un componente real el cual es una dependencia para el sujeto bajo prueba para que pueda verificar las salidas indirectas, el objeto Mock se encarga de verificar el comportamiento de los objetos durante la prueba, ya que es necesario mirar dentro del SUT, para determinar si el comportamiento esperado si se ha presentado.
¿Cómo funciona?
Lo primero que se debe hacer es definir el objeto Mock que es el encargado de sustituir la implementación del objeto real del que depende el sujeto bajo prueba. Luego durante la prueba se configura el objeto simulado con los valores con los que se le debe responder al sujeto bajo prueba y las llamadas al método completadas con los argumentos esperados del sujeto bajo prueba; cuando se llama el mock durante la ejecución del sujeto bajo prueba, este compara los argumentos reales recibidos con los esperados usando Aserciones de igualdad, en caso de que no coincidan los datos la prueba falla.
¿Cuándo usarlo?
Este tipo de test doubles es usado como punto de observación para verificar las salidas indirectas del SUT, también es usado cuando no se desea invocar el código de producción o cuando no resulta fácil verificar la ejecución de un código.
Ejemplo
Siguiendo el ejemplo de fake, crearemos una clase ServicioCalculadoraTest con JUnit4 para probar o testear nuestros métodos, recurriremos a la implementación de mock, su uso se justifica ya que esta clase tiene una dependencia con la interface ServicioDeDatos y para aislarla debemos falsearla esto sucede al llamar la anotación @mock, para ServicioDeDatos y realizar la inyección de dependencia; ServicioCalculadoraImplementacion el cual requiere el mock en su constructor, en su implementación si un método esperado no se llama, la prueba fallará, para el ejecicio, ese método sería obtenerListaNumeros(),
verificado por verify( servicioDatos ).obtenerListaNumeros ().
package test; import static org.junit.Assert.*; import static org.mockito.Mockito.verify; 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 clases.ServicioCalculadoraImplementacion; import interfaceSD.ServicioDeDatos; //Utilizará el Runner de Mockito para ejecutar nuestras pruebas @RunWith(MockitoJUnitRunner.class) public class ServicioCalculadoraTest { @InjectMocks private ServicioCalculadoraImplementacion servicioCalculadora; @Mock private ServicioDeDatos servicioDatos; @Test public void testCalcularPromedio() { when(servicioDatos.obtenerListaNumeros()).thenReturn(new int[] {1,2,3,4,5}); assertEquals(3.0,servicioCalculadora.calcularPromedio(), 0.0); verify(servicioDatos).obtenerListaNumeros(); } }
Una vez se realiza lo anterior, podemos ejecutar nuestra clase Test y vemos que el mock se implementó correctamente.

Este Test Double se utiliza para reemplazar aquel componente real que genera dependencia sobre el SUT para que la prueba pueda controlar las entradas indirectas de éste, es decir, se reemplaza a aquel objeto que responde con datos reales; por lo general el test Stub contiene una serie de datos predefinidos y se usan para responder a las llamadas que se hacen durante los test.
¿Cómo funciona?
Para hacer uso del test Stub, lo primero que se debe hacer es definir una implementación del objeto del que depende el sujeto bajo prueba o SUT, esta implementación debe ser configurada para que responda a las llamadas del sujeto bajo prueba, antes de preparar el SUT se debe definir el Stub para que se use en lugar de la implementación u objeto real, cuando el Stub es llamado por el SUT durante la ejecución de prueba, el test double devuelve los valores previamente definidos.
¿Cuándo usarlo?
El test Stub se puede utilizar como un punto de control en donde se busca controlar el comportamiento del sujeto bajo prueba con varias entradas indirectas sin necesidad de verificar las salidas indirectas, otra forma en la que podemos usar el Stub es cuando este inyecta valores simulando la información de un software no disponible en el entorno de pruebas, que ha sido llamado por el sujeto de pruebas, lo que permite que la prueba avance y no se quedes estancada al no tener los datos de software.
Ejemplo
Siguiendo el mismo ejemplo, vamos a crear un Stub, que nos permite verificar el estado en lugar del comportamiento. Es de aclarar que diferente a un Mock, a un stub no le importa si se llama o no, su función es simplemente proporcionar valores, esto lo hace a través del método when de mockito y el assert. Para este stub sería when (servicioDatos . obtenerNumeroRaiz()). thenReturn (4 ).
import static org.junit.Assert.*; import static org.mockito.Mockito.when; import java.util.ArrayList; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; import clases.ServicioCalculadoraImplementacion; import interfaceSD.ServicioDeDatos; //Utilizará el Runner de Mockito para ejecutar las pruebas @RunWith(MockitoJUnitRunner.class) public class ServicioCalculadoraTest { @InjectMocks private ServicioCalculadoraImplementacion servicioCalculadora; @Mock private ServicioDeDatos servicioDatos; @Test public void testCalcularRaizCuadrada() { when(servicioDatos.obtenerNumeroRaiz()).thenReturn(4); assertEquals(2.0,servicioCalculadora.calcularRaizCuadrada(), 0.0); } @Test //Calcula la formula para solucionar ecuaciones de segundo grado public void testCalcularFormulaGeneral() { //coeficiente a, b y c double [] esperado= {-0.033111854279919584, -10.06688814572008}; when(servicioDatos.obtenerCoeficientes()).thenReturn(new double[] {3.0,30.3,1.0}); assertArrayEquals(esperado, servicioCalculadora.calcularFormulaGeneral(), 0.0); } }
Con lo anterior, aseguramos que no se tenga una implementación real de los métodos, simplemente se «preprogramará» que si ese método obtenerRaiz es llamado, se obtendrá un resultado de 4; al ejecutar la prueba tendremos para ambos métodos un resultado
positivo como se observa a continuación.

Permite espiar, se utilizan cuando desea asegurarse de que un sistema haya llamado a un método incluyendo todos los detalles, también puede registrar todo tipo de cosas, como contar el número de invocaciones o mantener un registro de los argumentos pasados cada vez. Los espías se usan exclusivamente para verificar todo tipo de comportamiento.
¿Cómo funciona?
Antes de preparar el sujeto bajo prueba, se debe definir el test Spy como sustituto del objeto o clase real usado por el SUT, como lo mencionamos anteriormente el Spy está diseñado para verificar todo tipo de comportamiento, es por esto que durante la fase de verificación de resultados, la prueba compara los valores reales pasados al test Spy por el sujeto bajo prueba con los valores esperados por la prueba.
¿Cuándo Usarlo?
El test Spy es usado como punto de observación o verificación para las salidas indirectas del sujeto bajo prueba, es posible que el test Spy en algunos casos necesite proporcionar valores al sujeto bajo prueba en respuesta a llamadas a métodos, tal y como lo hace el test Stub, pero una de sus principales funciones es capturar las salidas indirectas del sujeto bajo prueba y guardarlas para verificarlas en su momento.
EjemploContinuando con el mismo ejercicio, para crear el doble de prueba spy, haremos uso de la anotación que nos ofrece mockito @Spy, para ServicioDeDatos. Añadiremos valores como se hizo con el Stub. Es de recordar que Spy, trabaja parcialmente con el objeto real.
package test; import static org.junit.Assert.*; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.verify; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Spy; import org.mockito.junit.MockitoJUnitRunner; import clases.ServicioCalculadoraImplementacion; import interfaceSD.ServicioDeDatos; //Utilizará el Runner de Mockito para ejecutar nuestras pruebas @RunWith(MockitoJUnitRunner.class) public class ServicioCalculadoraTest { @InjectMocks private ServicioCalculadoraImplementacion servicioCalculadora; @Spy private ServicioDeDatos spyServicioDatos; //Spy @Test //Calcula la formula para solucionar ecuaciones de segundo grado public void testCalcularFormulaGeneral() { //coeficiente a, b y c double [] esperado= {-0.033111854279919584, -10.06688814572008}; //Para Stubear un spy, es recomendado utilizar el doReturn en vez de when doReturn(new double[] {3.0,30.3,1.0}).when(spyServicioDatos).obtenerCoeficientes(); assertArrayEquals(esperado, servicioCalculadora.calcularFormulaGeneral(), 0.0); verify(spyServicioDatos).obtenerCoeficientes(); } }
El resultado será nuevamente exitoso

En un resumen más detallado de lo que acabamos de ver, tenemos que los objetos Dummy son realmente una alternativa a los patrones de valor, los test Stub se usan para verificar las entradas indirectas del SUT, el test Spy y el objeto Mock por lo general se usan para verificar salidas indirectas del SUT y realizar la correcta verificación y por último, los objetos Fake proporcionan una implementación alternativa, un poco más simple que el objeto real, todas estas variaciones que se tienen se clasifican según cómo o por qué usamos el Test Double.
Información a tener en cuenta
Es bueno tener en cuenta que ni los objetos Dummy ni Fake necesitan ser configurados, los objetos Dummy nunca deben ser utilizados por un receptor, por lo que no necesitan una implementación real, los objetos Fake por el contrario, necesitan una implementación real, pero se necesita que sea mucho más simple que el objeto que reemplazan, por lo tanto ni la prueba ni el autómata de prueba necesitarán configurar respuestas o expectativas para estos dos objetos, simplemente se deja que el sujeto bajo prueba los use como si fueran el objeto real.