Una guía para practicantes de pruebas unitarias para el Mockito cotidiano
Publicado: 2022-03-11Las pruebas unitarias se han vuelto obligatorias en la era de Agile, y hay muchas herramientas disponibles para ayudar con las pruebas automatizadas. Una de esas herramientas es Mockito, un marco de código abierto que le permite crear y configurar objetos simulados para pruebas.
En este artículo, cubriremos la creación y configuración de simulacros y su uso para verificar el comportamiento esperado del sistema que se está probando. También profundizaremos un poco en las partes internas de Mockito para comprender mejor su diseño y sus advertencias. Usaremos JUnit como un marco de prueba de unidad, pero dado que Mockito no está vinculado a JUnit, puede seguirlo incluso si está usando un marco diferente.
Obtención de Mockito
Conseguir Mockito es fácil en estos días. Si está utilizando Gradle, se trata de agregar esta única línea a su secuencia de comandos de compilación:
testCompile "org.mockito:mockito−core:2.7.7"
En cuanto a aquellos como yo que todavía prefieren Maven, simplemente agreguen Mockito a sus dependencias así:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>
Por supuesto, el mundo es mucho más amplio que Maven y Gradle. Puede utilizar cualquier herramienta de gestión de proyectos para obtener el artefacto del tarro de Mockito del repositorio central de Maven.
Acercándose a Mockito
Las pruebas unitarias están diseñadas para probar el comportamiento de clases o métodos específicos sin depender del comportamiento de sus dependencias. Como estamos probando la 'unidad' de código más pequeña, no necesitamos usar implementaciones reales de estas dependencias. Además, usaremos implementaciones ligeramente diferentes de estas dependencias al probar diferentes comportamientos. Un enfoque tradicional y bien conocido para esto es crear 'stubs': implementaciones específicas de una interfaz adecuada para un escenario determinado. Estas implementaciones suelen tener una lógica codificada. Un stub es una especie de doble de prueba. Otros tipos incluyen falsificaciones, simulacros, espías, maniquíes, etc.
Nos centraremos solo en dos tipos de dobles de prueba, 'simulacros' y 'espías', ya que Mockito los emplea mucho.
se burla
¿Qué es burlarse? Obviamente, no es donde te burlas de tus compañeros desarrolladores. La simulación para pruebas unitarias es cuando crea un objeto que implementa el comportamiento de un subsistema real de manera controlada. En resumen, los simulacros se utilizan como reemplazo de una dependencia.
Con Mockito, crea un simulacro, le dice a Mockito qué hacer cuando se le llama a métodos específicos y luego usa la instancia simulada en su prueba en lugar de la real. Después de la prueba, puede consultar el simulacro para ver qué métodos específicos se llamaron o verificar los efectos secundarios en forma de cambio de estado.
De forma predeterminada, Mockito proporciona una implementación para cada método del simulacro.
espías
Un espía es el otro tipo de doble de prueba que crea Mockito. A diferencia de los simulacros, la creación de un espía requiere una instancia para espiar. De forma predeterminada, un espía delega todas las llamadas de método al objeto real y registra qué método se llamó y con qué parámetros. Eso es lo que lo convierte en un espía: está espiando un objeto real.
Considere usar simulacros en lugar de espías siempre que sea posible. Los espías pueden ser útiles para probar el código heredado que no se puede rediseñar para que sea fácilmente comprobable, pero la necesidad de usar un espía para burlarse parcialmente de una clase es un indicador de que una clase está haciendo demasiado, violando así el principio de responsabilidad única.
Construyendo un ejemplo simple
Echemos un vistazo a una demostración simple para la que podemos escribir pruebas. Supongamos que tenemos una interfaz UserRepository
con un solo método para encontrar un usuario por su identificador. También tenemos el concepto de un codificador de contraseñas para convertir una contraseña de texto claro en un hash de contraseña. Tanto UserRepository
como PasswordEncoder
son dependencias (también llamadas colaboradores) de UserService
inyectadas a través del constructor. Así es como se ve nuestro código de demostración:
Repositorio de usuarios
public interface UserRepository { User findById(String id); }
Usuario
public class User { private String id; private String passwordHash; private boolean enabled; public User(String id, String passwordHash, boolean enabled) { this.id = id; this.passwordHash = passwordHash; this.enabled = enabled; } ... }
Codificador de contraseña
public interface PasswordEncoder { String encode(String password); }
Servicio de usuario
public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public boolean isValidUser(String id, String password) { User user = userRepository.findById(id); return isEnabledUser(user) && isValidPassword(user, password); } private boolean isEnabledUser(User user) { return user != null && user.isEnabled(); } private boolean isValidPassword(User user, String password) { String encodedPassword = passwordEncoder.encode(password); return encodedPassword.equals(user.getPasswordHash()); } }
Este código de ejemplo se puede encontrar en GitHub, por lo que puede descargarlo para revisarlo junto con este artículo.
Aplicando Mockito
Usando nuestro código de ejemplo, veamos cómo aplicar Mockito y escribir algunas pruebas.
Crear simulacros
Con Mockito, crear un simulacro es tan fácil como llamar a un método estático Mockito.mock()
:
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
Observe la importación estática para Mockito. Para el resto de este artículo, consideraremos implícitamente que se agregó esta importación.
Después de la importación, imitamos PasswordEncoder
, una interfaz. Mockito se burla no solo de las interfaces, sino también de las clases abstractas y las clases concretas no finales. Fuera de la caja, Mockito no puede simular clases finales y métodos finales o estáticos, pero si realmente lo necesita, Mockito 2 proporciona el complemento experimental MockMaker.
También tenga en cuenta que los métodos equals()
y hashCode()
no se pueden burlar.
Creando espías
Para crear un espía, debe llamar al método estático spy()
de Mockito y pasarle una instancia para espiar. Llamar a métodos del objeto devuelto llamará a métodos reales a menos que esos métodos sean stub. Estas llamadas se registran y los hechos de estas llamadas se pueden verificar (consulte una descripción más detallada de verify()
). Hagamos un espía:
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));
Crear un espía no difiere mucho de crear un simulacro. Además, todos los métodos de Mockito utilizados para configurar un simulacro también se aplican a la configuración de un espía.
Los espías rara vez se usan en comparación con los simulacros, pero puede encontrarlos útiles para probar código heredado que no se puede refactorizar, donde la prueba requiere una simulación parcial. En esos casos, puede simplemente crear un espía y agregar algunos de sus métodos para obtener el comportamiento que desea.
Valores de retorno predeterminados
Llamar a mock(PasswordEncoder.class)
devuelve una instancia de PasswordEncoder
. Incluso podemos llamar a sus métodos, pero ¿qué devolverán? De forma predeterminada, todos los métodos de un simulacro devuelven valores "no inicializados" o "vacíos", por ejemplo, ceros para los tipos numéricos (tanto primitivos como en caja), falsos para los booleanos y nulos para la mayoría de los demás tipos.
Considere la siguiente interfaz:
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }
Ahora considere el siguiente fragmento, que da una idea de qué valores predeterminados esperar de los métodos de un simulacro:
Demo demo = mock(Demo.class); assertEquals(0, demo.getInt()); assertEquals(0, demo.getInteger().intValue()); assertEquals(0d, demo.getDouble(), 0d); assertFalse(demo.getBoolean()); assertNull(demo.getObject()); assertEquals(Collections.emptyList(), demo.getCollection()); assertNull(demo.getArray()); assertEquals(0L, demo.getStream().count()); assertFalse(demo.getOptional().isPresent());
Métodos de troquelado
Los simulacros frescos e inalterados son útiles solo en casos excepcionales. Por lo general, queremos configurar el simulacro y definir qué hacer cuando se llaman métodos específicos del simulacro. Esto se llama stubing .
Mockito ofrece dos formas de stubbing. La primera forma es " cuando se llama a este método, entonces haz algo". Considere el siguiente fragmento:
when(passwordEncoder.encode("1")).thenReturn("a");
Se lee casi como en inglés: "Cuando se llama a passwordEncoder.encode(“1”)
, devuelve una a
".
La segunda forma de creación de apéndices se lee más como "Haz algo cuando se llame al método de este simulacro con los siguientes argumentos". Esta forma de stubing es más difícil de leer ya que la causa se especifica al final. Considerar:
doReturn("a").when(passwordEncoder).encode("1");
El fragmento con este método de creación de apéndices diría: "Devolver a
cuando se llama al método encode()
de passwordEncoder
con un argumento de 1
".
La primera forma se considera preferida porque es seguro para los tipos y porque es más legible. Sin embargo, en raras ocasiones, te verás obligado a usar la segunda forma, como cuando bloqueas un método real de un espía porque llamarlo puede tener efectos secundarios no deseados.
Exploremos brevemente los métodos de stub proporcionados por Mockito. Incluiremos ambas formas de creación de apéndices en nuestros ejemplos.
Valores devueltos
thenReturn
o doReturn()
se utilizan para especificar un valor que se devolverá al invocar el método.
//”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");
o
//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");
También puede especificar varios valores que se devolverán como resultados de llamadas de método consecutivas. El último valor se utilizará como resultado para todas las demás llamadas a métodos.
//when when(passwordEncoder.encode("1")).thenReturn("a", "b");
o
//do doReturn("a", "b").when(passwordEncoder).encode("1");
Lo mismo se puede lograr con el siguiente fragmento:
when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");
Este patrón también se puede utilizar con otros métodos de creación de apéndices para definir los resultados de llamadas consecutivas.
Devolución de respuestas personalizadas
then()
, un alias de thenAnswer()
y doAnswer()
logran lo mismo, que es configurar una respuesta personalizada para que se devuelva cuando se llama a un método, así:
when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");
o
doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");
El único argumento thenAnswer()
es una implementación de la interfaz Answer
. Tiene un único método con un parámetro de tipo InvocationOnMock
.
También puede generar una excepción como resultado de una llamada a un método:
when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
…o llame al método real de una clase (no aplicable a las interfaces):
Date mock = mock(Date.class); doAnswer(InvocationOnMock::callRealMethod).when(mock).setTime(42); doAnswer(InvocationOnMock::callRealMethod).when(mock).getTime(); mock.setTime(42); assertEquals(42, mock.getTime());
Tienes razón si crees que parece engorroso. Mockito proporciona thenCallRealMethod()
y thenThrow()
para agilizar este aspecto de sus pruebas.
Llamar a métodos reales
Como sugiere su nombre, thenCallRealMethod()
y doCallRealMethod()
llaman al método real en un objeto simulado:
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
Llamar a métodos reales puede ser útil en simulacros parciales, pero asegúrese de que el método llamado no tenga efectos secundarios no deseados y no dependa del estado del objeto. Si es así, un espía puede encajar mejor que un simulacro.
Si crea una interfaz simulada e intenta configurar un código auxiliar para llamar a un método real, Mockito generará una excepción con un mensaje muy informativo. Considere el siguiente fragmento:
when(passwordEncoder.encode("1")).thenCallRealMethod();
Mockito fallará con el siguiente mensaje:
Cannot call abstract real method on java object! Calling real methods is only possible when mocking non abstract method. //correct example: when(mockOfConcreteClass.nonAbstractMethod()).thenCallRealMethod();
¡Felicitaciones a los desarrolladores de Mockito por preocuparse lo suficiente como para proporcionar descripciones tan completas!
Lanzar excepciones
thenThrow()
y doThrow()
configuran un método simulado para lanzar una excepción:
when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());
o
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");
Mockito se asegura de que la excepción que se lanza sea válida para ese método auxiliar específico y se quejará si la excepción no está en la lista de excepciones verificadas del método. Considera lo siguiente:
when(passwordEncoder.encode("1")).thenThrow(new IOException());
Dará lugar a un error:
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
Como puede ver, Mockito detectó que encode()
no puede generar una IOException
.
También puede pasar una clase de excepción en lugar de pasar una instancia de una excepción:
when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);
o
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");
Dicho esto, Mockito no puede validar una clase de excepción de la misma manera que validaría una instancia de excepción, por lo que debe ser disciplinado y no pasar objetos de clase ilegales. Por ejemplo, lo siguiente arrojará IOException
aunque no se espera que encode()
arroje una excepción marcada:
when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");
Simulación de interfaces con métodos predeterminados
Vale la pena señalar que al crear un simulacro para una interfaz, Mockito simula todos los métodos de esa interfaz. Desde Java 8, las interfaces pueden contener métodos predeterminados junto con métodos abstractos. Estos métodos también se simulan, por lo que debe tener cuidado de que actúen como métodos predeterminados.
Considere el siguiente ejemplo:
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
En este ejemplo, assertFalse()
tendrá éxito. Si eso no es lo que esperaba, asegúrese de que Mockito haya llamado al método real, así:
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
Coincidencias de argumentos
En las secciones anteriores, configuramos nuestros métodos simulados con valores exactos como argumentos. En esos casos, Mockito simplemente llama a equals()
internamente para verificar si los valores esperados son iguales a los valores reales.
A veces, sin embargo, no conocemos estos valores de antemano.
Tal vez simplemente no nos importa el valor real que se pasa como argumento, o tal vez queremos definir una reacción para un rango más amplio de valores. Todos estos escenarios (y más) se pueden abordar con comparadores de argumentos. La idea es simple: en lugar de proporcionar un valor exacto, proporciona un comparador de argumentos para que Mockito coincida con los argumentos del método.
Considere el siguiente fragmento:
when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));
Puede ver que el resultado es el mismo sin importar el valor que le pasemos a encode()
porque usamos el comparador de argumentos anyString anyString()
en esa primera línea. Si reescribimos esa línea en un lenguaje sencillo, sonaría como "cuando se le pide al codificador de contraseña que codifique cualquier cadena, entonces devuelve la cadena 'exacta'".
Mockito requiere que proporcione todos los argumentos ya sea por comparadores o por valores exactos. Entonces, si un método tiene más de un argumento y desea usar comparadores de argumentos solo para algunos de sus argumentos, olvídelo. No puedes escribir código como este:
abstract class AClass { public abstract boolean call(String s, int i); } AClass mock = mock(AClass.class); //This doesn't work. when(mock.call("a", anyInt())).thenReturn(true);
Para corregir el error, debemos reemplazar la última línea para incluir el argumento eq
matcher para a
, de la siguiente manera:
when(mock.call(eq("a"), anyInt())).thenReturn(true);
Aquí hemos usado los comparadores de argumentos eq()
y anyInt()
, pero hay muchos otros disponibles. Para obtener una lista completa de comparadores de argumentos, consulte la documentación de la clase org.mockito.ArgumentMatchers
.
Es importante tener en cuenta que no puede usar comparadores de argumentos fuera de la verificación o la creación de apéndices. Por ejemplo, no puede tener lo siguiente:
//this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);
Mockito detectará el comparador de argumentos fuera de lugar y lanzará una InvalidUseOfMatchersException
. La verificación con comparadores de argumentos debe hacerse de esta manera:
verify(mock).encode(or(eq("a"), endsWith("b")));
Los comparadores de argumentos tampoco se pueden usar como valor de retorno. Mockito no puede devolver anyString()
o lo que sea; se requiere un valor exacto cuando se agregan llamadas.
Emparejadores personalizados
Los emparejadores personalizados vienen al rescate cuando necesita proporcionar alguna lógica de coincidencia que aún no está disponible en Mockito. La decisión de crear un comparador personalizado no debe tomarse a la ligera, ya que la necesidad de hacer coincidir los argumentos de una manera no trivial indica un problema en el diseño o que una prueba se está volviendo demasiado complicada.
Como tal, vale la pena verificar si puede simplificar una prueba usando algunos de los comparadores de argumentos indulgentes como isNull()
y nullable()
antes de escribir un comparador personalizado. Si aún siente la necesidad de escribir un comparador de argumentos, Mockito proporciona una familia de métodos para hacerlo.
Considere el siguiente ejemplo:
FileFilter fileFilter = mock(FileFilter.class); ArgumentMatcher<File> hasLuck = file -> file.getName().endsWith("luck"); when(fileFilter.accept(argThat(hasLuck))).thenReturn(true); assertFalse(fileFilter.accept(new File("/deserve"))); assertTrue(fileFilter.accept(new File("/deserve/luck")));
Aquí creamos el hasLuck
argumentos hasLuck y usamos argThat()
para pasar el comparador como un argumento a un método simulado, tachándolo para que devuelva true
si el nombre del archivo termina con "suerte". Puede tratar a ArgumentMatcher
como una interfaz funcional y crear su instancia con una lambda (que es lo que hemos hecho en el ejemplo). Una sintaxis menos concisa se vería así:
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
Si necesita crear un comparador de argumentos que funcione con tipos primitivos, hay varios otros métodos para eso en org.mockito.ArgumentMatchers
:
- charThat(ArgumentMatcher<Carácter> comparador)
- booleanThat(ArgumentMatcher<Boolean> comparador)
- byteThat(ArgumentMatcher<Byte> comparador)
- shortThat(ArgumentMatcher<Short> comparador)
- intThat(ArgumentMatcher<Integer> comparador)
- longThat(ArgumentMatcher<Long> comparador)
- floatThat(ArgumentMatcher<Float> comparador)
- doubleThat(ArgumentMatcher<Double> comparador)
Combinar emparejadores
No siempre vale la pena crear un comparador de argumentos personalizado cuando una condición es demasiado complicada para ser manejada con comparadores básicos; a veces la combinación de emparejadores hará el truco. Mockito proporciona comparadores de argumentos para implementar operaciones lógicas comunes ("no", "y", "o") en comparadores de argumentos que coinciden con tipos primitivos y no primitivos. Estos comparadores se implementan como métodos estáticos en la clase org.mockito.AdditionalMatchers
.
Considere el siguiente ejemplo:
when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));
Aquí hemos combinado los resultados de dos comparadores de argumentos: eq("1")
y contains("a")
. La expresión final, or(eq("1"), contains("a"))
, puede interpretarse como "la cadena del argumento debe ser igual a "1" o contener "a".
Tenga en cuenta que hay comparadores menos comunes enumerados en la clase org.mockito.AdditionalMatchers
, como geq()
, leq()
, gt()
y lt()
, que son comparaciones de valores aplicables para valores primitivos e instancias de java.lang.Comparable
.

Comportamiento de verificación
Una vez que se ha utilizado un simulacro o un espía, podemos verify
que se produjeron interacciones específicas. Literalmente, estamos diciendo "Oye, Mockito, asegúrate de que este método se haya llamado con estos argumentos".
Considere el siguiente ejemplo artificial:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");
Aquí configuramos un simulacro y llamamos a su método encode()
. La última línea verifica que se llamó al método encode()
del simulacro con el valor de argumento específico a
. Tenga en cuenta que verificar una invocación stubed es redundante; el propósito del fragmento anterior es mostrar la idea de hacer una verificación después de que ocurrieron algunas interacciones.
Si cambiamos la última línea para que tenga un argumento diferente, por ejemplo, b
, la prueba anterior fallará y Mockito se quejará de que la invocación real tiene argumentos diferentes ( b
en lugar de la esperada a
).
Los comparadores de argumentos se pueden usar para la verificación al igual que para la creación de apéndices:
verify(passwordEncoder).encode(anyString());
De forma predeterminada, Mockito verifica que el método se haya llamado una vez, pero puede verificar cualquier número de invocaciones:
// verify the exact number of invocations verify(passwordEncoder, times(42)).encode(anyString()); // verify that there was at least one invocation verify(passwordEncoder, atLeastOnce()).encode(anyString()); // verify that there were at least five invocations verify(passwordEncoder, atLeast(5)).encode(anyString()); // verify the maximum number of invocations verify(passwordEncoder, atMost(5)).encode(anyString()); // verify that it was the only invocation and // that there're no more unverified interactions verify(passwordEncoder, only()).encode(anyString()); // verify that there were no invocations verify(passwordEncoder, never()).encode(anyString());
Una función raramente utilizada de verify()
es su capacidad de fallar en un tiempo de espera, lo que es principalmente útil para probar código concurrente. Por ejemplo, si nuestro codificador de contraseñas se llama en otro subproceso al mismo tiempo que verify()
, podemos escribir una prueba de la siguiente manera:
usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");
Esta prueba tendrá éxito si se llama a encode()
y finaliza en 500 milisegundos o menos. Si necesita esperar el período completo que especifique, use after()
en lugar de timeout()
:
verify(passwordEncoder, after(500)).encode("a");
Otros modos de verificación ( times()
, atLeast()
, etc.) se pueden combinar con timeout()
y after()
para hacer pruebas más complicadas:
// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");
Además de times()
, los modos de verificación admitidos incluyen only()
, atLeast()
y atLeastOnce()
(como un alias de atLeast(1)
).
Mockito también le permite verificar el orden de llamada en un grupo de simulacros. No es una función que se use con mucha frecuencia, pero puede ser útil si el orden de las invocaciones es importante. Considere el siguiente ejemplo:
PasswordEncoder first = mock(PasswordEncoder.class); PasswordEncoder second = mock(PasswordEncoder.class); // simulate calls first.encode("f1"); second.encode("s1"); first.encode("f2"); // verify call order InOrder inOrder = inOrder(first, second); inOrder.verify(first).encode("f1"); inOrder.verify(second).encode("s1"); inOrder.verify(first).encode("f2");
Si reorganizamos el orden de las llamadas simuladas, la prueba fallará con VerificationInOrderFailure
.
La ausencia de invocaciones también se puede verificar usando verificarZeroInteractions verifyZeroInteractions()
. Este método acepta un simulacro o simulacros como argumento y fallará si se llama a cualquiera de los métodos de los simulacros pasados.
También vale la pena mencionar el método verifyNoMoreInteractions()
, ya que toma simulacros como argumento y se puede usar para verificar que cada llamada en esos simulacros se haya verificado.
Captura de argumentos
Además de verificar que se llamó a un método con argumentos específicos, Mockito le permite capturar esos argumentos para que luego pueda ejecutar afirmaciones personalizadas sobre ellos. En otras palabras, estás diciendo "Oye, Mockito, verifica que se haya llamado a este método y dame los valores de argumento con los que se llamó".
Vamos a crear un simulacro de PasswordEncoder
, llamar a encode()
, capturar el argumento y verificar su valor:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("password", passwordCaptor.getValue());
Como puede ver, pasamos passwordCaptor.capture()
como argumento de encode()
para verificación; esto crea internamente un comparador de argumentos que guarda el argumento. Luego recuperamos el valor capturado con passwordCaptor.getValue()
y lo inspeccionamos con assertEquals()
.
Si necesitamos capturar un argumento en varias llamadas, ArgumentCaptor
le permite recuperar todos los valores con getAllValues()
, así:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); passwordEncoder.encode("password1"); passwordEncoder.encode("password2"); passwordEncoder.encode("password3"); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder, times(3)).encode(passwordCaptor.capture()); assertEquals(Arrays.asList("password1", "password2", "password3"), passwordCaptor.getAllValues());
La misma técnica se puede utilizar para capturar argumentos de método de aridad variable (también conocidos como varargs).
Probando nuestro ejemplo simple
Ahora que sabemos mucho más sobre Mockito, es hora de volver a nuestra demostración. Escribamos la prueba del método isValidUser
. Esto es lo que podría parecer:
public class UserServiceTest { private static final String PASSWORD = "password"; private static final User ENABLED_USER = new User("user id", "hash", true); private static final User DISABLED_USER = new User("disabled user id", "disabled user password hash", false); private UserRepository userRepository; private PasswordEncoder passwordEncoder; private UserService userService; @Before public void setup() { userRepository = createUserRepository(); passwordEncoder = createPasswordEncoder(); userService = new UserService(userRepository, passwordEncoder); } @Test public void shouldBeValidForValidCredentials() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), PASSWORD); assertTrue(userIsValid); // userRepository had to be used to find a user with verify(userRepository).findById(ENABLED_USER.getId()); // passwordEncoder had to be used to compute a hash of "password" verify(passwordEncoder).encode(PASSWORD); } @Test public void shouldBeInvalidForInvalidId() { boolean userIsValid = userService.isValidUser("invalid id", PASSWORD); assertFalse(userIsValid); InOrder inOrder = inOrder(userRepository, passwordEncoder); inOrder.verify(userRepository).findById("invalid id"); inOrder.verify(passwordEncoder, never()).encode(anyString()); } @Test public void shouldBeInvalidForInvalidPassword() { boolean userIsValid = userService.isValidUser(ENABLED_USER.getId(), "invalid"); assertFalse(userIsValid); ArgumentCaptor<String> passwordCaptor = ArgumentCaptor.forClass(String.class); verify(passwordEncoder).encode(passwordCaptor.capture()); assertEquals("invalid", passwordCaptor.getValue()); } @Test public void shouldBeInvalidForDisabledUser() { boolean userIsValid = userService.isValidUser(DISABLED_USER.getId(), PASSWORD); assertFalse(userIsValid); verify(userRepository).findById(DISABLED_USER.getId()); verifyZeroInteractions(passwordEncoder); } private PasswordEncoder createPasswordEncoder() { PasswordEncoder mock = mock(PasswordEncoder.class); when(mock.encode(anyString())).thenReturn("any password hash"); when(mock.encode(PASSWORD)).thenReturn(ENABLED_USER.getPasswordHash()); return mock; } private UserRepository createUserRepository() { UserRepository mock = mock(UserRepository.class); when(mock.findById(ENABLED_USER.getId())).thenReturn(ENABLED_USER); when(mock.findById(DISABLED_USER.getId())).thenReturn(DISABLED_USER); return mock; } }
Buceando bajo la API
Mockito proporciona una API legible y conveniente, pero exploremos algunos de sus funcionamientos internos para comprender sus limitaciones y evitar errores extraños.
Examinemos lo que sucede dentro de Mockito cuando se ejecuta el siguiente fragmento:
// 1: create PasswordEncoder mock = mock(PasswordEncoder.class); // 2: stub when(mock.encode("a")).thenReturn("1"); // 3: act mock.encode("a"); // 4: verify verify(mock).encode(or(eq("a"), endsWith("b")));
Obviamente, la primera línea crea un simulacro. Mockito usa ByteBuddy para crear una subclase de la clase dada. El nuevo objeto de clase tiene un nombre generado como demo.mockito.PasswordEncoder$MockitoMock$1953422997
, su equals()
actuará como verificación de identidad y hashCode()
devolverá un código hash de identidad. Una vez que la clase se genera y carga, su instancia se crea usando Objenesis.
Veamos la siguiente línea:
when(mock.encode("a")).thenReturn("1");
El orden es importante: la primera instrucción que se ejecuta aquí es mock.encode("a")
, que invocará encode()
en el simulacro con un valor de retorno predeterminado null
. Realmente, estamos pasando null
como argumento de when()
. A Mockito no le importa qué valor exacto se pasa a when()
porque almacena información sobre la invocación de un método simulado en el llamado 'stubbing continuo' cuando se invoca ese método. Más tarde, cuando llamamos a when()
, Mockito extrae ese objeto de creación de apéndices en curso y lo devuelve como resultado de when()
. Luego llamamos a thenReturn(“1”)
en el objeto de creación de apéndices en curso devuelto.
La tercera línea, mock.encode("a");
es simple: estamos llamando al método stubbed. Internamente, Mockito guarda esta invocación para verificación adicional y devuelve la respuesta de invocación añadida; en nuestro caso, es la cadena 1
.
En la cuarta línea ( verify(mock).encode(or(eq("a"), endsWith("b")));
), le pedimos a Mockito que verifique que hubo una invocación de encode()
con esos argumentos específicos.
verify()
se ejecuta primero, lo que convierte el estado interno de Mockito en modo de verificación. Es importante entender que Mockito mantiene su estado en un ThreadLocal
. Esto hace posible implementar una buena sintaxis pero, por otro lado, puede generar un comportamiento extraño si el marco se usa incorrectamente (si intenta usar comparadores de argumentos fuera de la verificación o la creación de apéndices, por ejemplo).
Entonces, ¿cómo crea Mockito un or
matcher? Primero, se llama eq("a")
y se agrega un comparador de equals
a la pila de emparejadores. En segundo lugar, se llama a endsWith
endsWith("b")
y se agrega un comparador de extremos con a la pila. Por último, se llama or(null, null)
: utiliza los dos comparadores que extrae de la pila, crea el comparador or
y lo empuja a la pila. Finalmente, se llama a encode()
. Luego, Mockito verifica que el método se haya invocado la cantidad de veces esperada y con los argumentos esperados.
Si bien los comparadores de argumentos no se pueden extraer a variables (porque cambia el orden de llamada), se pueden extraer a métodos. Esto conserva el orden de las llamadas y mantiene la pila en el estado correcto:
verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }
Cambio de respuestas predeterminadas
En secciones anteriores, creamos nuestros simulacros de tal manera que cuando se llama a cualquier método simulado, devuelven un valor "vacío". Este comportamiento es configurable. Incluso puede proporcionar su propia implementación de org.mockito.stubbing.Answer
si las proporcionadas por Mockito no son adecuadas, pero podría ser una indicación de que algo anda mal cuando las pruebas unitarias se vuelven demasiado complicadas. ¡Recuerda el principio KISS!
Exploremos la oferta de Mockito de respuestas predeterminadas predefinidas:
RETURNS_DEFAULTS
es la estrategia predeterminada; no vale la pena mencionarlo explícitamente al configurar un simulacro.CALLS_REAL_METHODS
hace que las invocaciones sin stub llamen a métodos reales.RETURNS_SMART_NULLS
evita una excepciónNullPointerException
devolviendoSmartNull
en lugar denull
cuando se usa un objeto devuelto por una llamada de método sin stub. Todavía fallará con unaNullPointerException
, peroSmartNull
le brinda un seguimiento de pila más agradable con la línea donde se llamó al método sin stubbed. ¡Esto hace que valga la pena queRETURNS_SMART_NULLS
sea la respuesta predeterminada en Mockito!RETURNS_MOCKS
primero intenta devolver valores "vacíos" ordinarios, luego simula, si es posible, ynull
de lo contrario. El criterio de vacío difiere un poco de lo que hemos visto anteriormente: en lugar de devolver unnull
para cadenas y matrices, los simulacros creados conRETURNS_MOCKS
devuelven cadenas y matrices vacías, respectivamente.RETURNS_SELF
es útil para burlarse de los constructores. Con esta configuración, un simulacro devolverá una instancia de sí mismo si se llama a un método que devuelve algo de un tipo igual a la clase (o una superclase) de la clase simulada.RETURNS_DEEP_STUBS
va más allá queRETURNS_MOCKS
y crea simulacros que pueden devolver simulacros de simulacros de simulacros, etc. En contraste conRETURNS_MOCKS
, las reglas de vacío son predeterminadas enRETURNS_DEEP_STUBS
, por lo que devuelvenull
para cadenas y matrices:
interface We { Are we(); } interface Are { So are(); } interface So { Deep so(); } interface Deep { boolean deep(); } ... We mock = mock(We.class, Mockito.RETURNS_DEEP_STUBS); when(mock.we().are().so().deep()).thenReturn(true); assertTrue(mock.we().are().so().deep());
Nombrar un simulacro
Mockito le permite nombrar un simulacro, una función útil si tiene muchos simulacros en una prueba y necesita distinguirlos. Dicho esto, la necesidad de nombrar simulacros puede ser un síntoma de un diseño deficiente. Considera lo siguiente:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Mockito se quejará, pero debido a que no hemos nombrado formalmente los simulacros, no sabemos cuál:
Wanted but not invoked: passwordEncoder.encode(<any string>);
Vamos a nombrarlos pasando una cadena en la construcción:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());
Ahora el mensaje de error es más amigable y apunta claramente a robustPasswordEncoder
:
Wanted but not invoked: robustPasswordEncoder.encode(<any string>);
Implementación de múltiples interfaces simuladas
Sometimes, you may wish to create a mock that implements several interfaces. Mockito is able to do that easily, like so:
PasswordEncoder mock = mock( PasswordEncoder.class, withSettings().extraInterfaces(List.class, Map.class)); assertTrue(mock instanceof List); assertTrue(mock instanceof Map);
Listening Invocations
A mock can be configured to call an invocation listener every time a method of the mock was called. Inside the listener, you can find out whether the invocation produced a value or if an exception was thrown.
InvocationListener invocationListener = new InvocationListener() { @Override public void reportInvocation(MethodInvocationReport report) { if (report.threwException()) { Throwable throwable = report.getThrowable(); // do something with throwable throwable.printStackTrace(); } else { Object returnedValue = report.getReturnedValue(); // do something with returnedValue System.out.println(returnedValue); } } }; PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().invocationListeners(invocationListener)); passwordEncoder.encode("1");
In this example, we're dumping either the returned value or a stack trace to a system output stream. Our implementation does roughly the same as Mockito's org.mockito.internal.debugging.VerboseMockInvocationLogger
(don't use this directly, it's internal stuff). If logging invocations is the only feature you need from the listener, then Mockito provides a cleaner way to express your intent with the verboseLogging()
setting:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging());
Take notice, though, that Mockito will call the listeners even when you're stubbing methods. Considere el siguiente ejemplo:
PasswordEncoder passwordEncoder = mock( PasswordEncoder.class, withSettings().verboseLogging()); // listeners are called upon encode() invocation when(passwordEncoder.encode("1")).thenReturn("encoded1"); passwordEncoder.encode("1"); passwordEncoder.encode("2");
This snippet will produce an output similar to the following:
############ Logging method invocation #1 on mock/spy ######## passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) has returned: "null" ############ Logging method invocation #2 on mock/spy ######## stubbed: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:85) passwordEncoder.encode("1"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:89) has returned: "encoded1" (java.lang.String) ############ Logging method invocation #3 on mock/spy ######## passwordEncoder.encode("2"); invoked: -> at demo.mockito.MockSettingsTest.verboseLogging(MockSettingsTest.java:90) has returned: "null"
Note that the first logged invocation corresponds to calling encode()
while stubbing it. It's the next invocation that corresponds to calling the stubbed method.
Otros ajustes
Mockito offers a few more settings that let you do the following:
- Enable mock serialization by using
withSettings().serializable()
. - Turn off recording of method invocations to save memory (this will make verification impossible) by using
withSettings().stubOnly()
. - Use the constructor of a mock when creating its instance by using
withSettings().useConstructor()
. When mocking inner non-static classes, add anouterInstance()
setting, like so:withSettings().useConstructor().outerInstance(outerObject)
.
If you need to create a spy with custom settings (such as a custom name), there's a spiedInstance()
setting, so that Mockito will create a spy on the instance you provide, like so:
UserService userService = new UserService( mock(UserRepository.class), mock(PasswordEncoder.class)); UserService userServiceMock = mock( UserService.class, withSettings().spiedInstance(userService).name("coolService"));
When a spied instance is specified, Mockito will create a new instance and populate its non-static fields with values from the original object. That's why it's important to use the returned instance: Only its method calls can be stubbed and verified.
Note that, when you create a spy, you're basically creating a mock that calls real methods:
// creating a spy this way... spy(userService); // ... is a shorthand for mock(UserService.class, withSettings() .spiedInstance(userService) .defaultAnswer(CALLS_REAL_METHODS));
When Mockito Tastes Bad
It's our bad habits that make our tests complex and unmaintainable, not Mockito. For example, you may feel the need to mock everything. This kind of thinking leads to testing mocks instead of production code. Mocking third-party APIs can also be dangerous due to potential changes in that API that can break the tests.
Though bad taste is a matter of perception, Mockito provides a few controversial features that can make your tests less maintainable. Sometimes stubbing isn't trivial, or an abuse of dependency injection can make recreating mocks for each test difficult, unreasonable or inefficient.
Clearing Invocations
Mockito allows for clearing invocations for mocks while preserving stubbing, like so:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); UserRepository userRepository = mock(UserRepository.class); // use mocks passwordEncoder.encode(null); userRepository.findById(null); // clear clearInvocations(passwordEncoder, userRepository); // succeeds because invocations were cleared verifyZeroInteractions(passwordEncoder, userRepository);
Resort to clearing invocations only if recreating a mock would lead to significant overhead or if a configured mock is provided by a dependency injection framework and stubbing is non-trivial.
Resetting a Mock
Resetting a mock with reset()
is another controversial feature and should be used in extremely rare cases, like when a mock is injected by a container and you can't recreate it for each test.
Overusing Verify
Another bad habit is trying to replace every assert with Mockito's verify()
. It's important to clearly understand what is being tested: interactions between collaborators can be checked with verify()
, while confirming the observable results of an executed action is done with asserts.
Mockito Is about Frame of Mind
Using Mockito is not just a matter of adding another dependency, it requires changing how you think about your unit tests while removing a lot of boilerplate.
With multiple mock interfaces, listening invocations, matchers and argument captors, we've seen how Mockito makes your tests cleaner and easier to understand, but like any tool, it must be used appropriately to be useful. Now armed with the knowledge of Mockito's inner workings, you can take your unit testing to the next level.