Um Guia do Praticante de Testes Unitários para Mockito Diário
Publicados: 2022-03-11O teste de unidade tornou-se obrigatório na era do Agile, e existem muitas ferramentas disponíveis para ajudar nos testes automatizados. Uma dessas ferramentas é o Mockito, uma estrutura de código aberto que permite criar e configurar objetos simulados para testes.
Neste artigo, abordaremos a criação e configuração de mocks e seu uso para verificar o comportamento esperado do sistema que está sendo testado. Também vamos mergulhar um pouco nas partes internas do Mockito para entender melhor seu design e ressalvas. Usaremos o JUnit como um framework de teste de unidade, mas como o Mockito não está vinculado ao JUnit, você pode acompanhar mesmo se estiver usando um framework diferente.
Obtendo Mockito
Conseguir Mockito é fácil hoje em dia. Se você estiver usando o Gradle, é uma questão de adicionar esta única linha ao seu script de compilação:
testCompile "org.mockito:mockito−core:2.7.7"
Para aqueles como eu que ainda preferem o Maven, basta adicionar o Mockito às suas dependências assim:
<dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>2.7.7</version> <scope>test</scope> </dependency>
Claro, o mundo é muito mais amplo do que Maven e Gradle. Você pode usar qualquer ferramenta de gerenciamento de projetos para buscar o artefato jar do Mockito no repositório central do Maven.
Aproximando-se de Mockito
Os testes de unidade são projetados para testar o comportamento de classes ou métodos específicos sem depender do comportamento de suas dependências. Como estamos testando a menor 'unidade' de código, não precisamos usar implementações reais dessas dependências. Além disso, usaremos implementações ligeiramente diferentes dessas dependências ao testar diferentes comportamentos. Uma abordagem tradicional e bem conhecida para isso é criar 'stubs' – implementações específicas de uma interface adequada para um determinado cenário. Essas implementações geralmente têm lógica codificada. Um stub é uma espécie de teste duplo. Outros tipos incluem falsificações, mocks, espiões, manequins, etc.
Estaremos focando apenas em dois tipos de dublês de teste, 'mocks' e 'spies', já que estes são muito empregados por Mockito.
Simulações
O que é zombar? Obviamente, não é onde você tira sarro de seus colegas desenvolvedores. Zombar para teste de unidade é quando você cria um objeto que implementa o comportamento de um subsistema real de maneiras controladas. Resumindo, mocks são usados como substitutos para uma dependência.
Com o Mockito, você cria um mock, diz ao Mockito o que fazer quando métodos específicos são chamados nele e, em seguida, usa a instância mock em seu teste em vez da real. Após o teste, você pode consultar o mock para ver quais métodos específicos foram chamados ou verificar os efeitos colaterais na forma de estado alterado.
Por padrão, o Mockito fornece uma implementação para cada método do mock.
Espiões
Um espião é o outro tipo de dublê de teste que Mockito cria. Ao contrário dos mocks, criar um espião requer uma instância para espionar. Por padrão, um espião delega todas as chamadas de método ao objeto real e registra qual método foi chamado e com quais parâmetros. É isso que o torna um espião: está espionando um objeto real.
Considere usar mocks em vez de espiões sempre que possível. Espiões podem ser úteis para testar código legado que não pode ser redesenhado para ser facilmente testável, mas a necessidade de usar um espião para zombar parcialmente de uma classe é um indicador de que uma classe está fazendo muito, violando assim o princípio de responsabilidade única.
Construindo um Exemplo Simples
Vamos dar uma olhada em uma demonstração simples para a qual podemos escrever testes. Suponha que tenhamos uma interface UserRepository
com um único método para encontrar um usuário por seu identificador. Também temos o conceito de um codificador de senha para transformar uma senha de texto simples em um hash de senha. Tanto UserRepository
quanto PasswordEncoder
são dependências (também chamadas de colaboradores) de UserService
injetadas por meio do construtor. Veja como nosso código de demonstração se parece:
UserRepository
public interface UserRepository { User findById(String id); }
Do utilizador
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 Senha
public interface PasswordEncoder { String encode(String password); }
UserService
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 exemplo pode ser encontrado no GitHub, para que você possa baixá-lo para revisão ao lado deste artigo.
Aplicando Mockito
Usando nosso código de exemplo, vamos ver como aplicar o Mockito e escrever alguns testes.
Criando simulados
Com o Mockito, criar um mock é tão fácil quanto chamar um método estático Mockito.mock()
:
import static org.mockito.Mockito.*; ... PasswordEncoder passwordEncoder = mock(PasswordEncoder.class);
Observe a importação estática para Mockito. No restante deste artigo, consideraremos implicitamente essa importação adicionada.
Após a importação, simulamos PasswordEncoder
, uma interface. Mockito zomba não apenas de interfaces, mas também de classes abstratas e classes não-finais concretas. Fora da caixa, o Mockito não pode simular classes finais e métodos finais ou estáticos, mas se você realmente precisar, o Mockito 2 fornece o plug-in experimental MockMaker.
Observe também que os métodos equals()
e hashCode()
não podem ser simulados.
Criando espiões
Para criar um espião, você precisa chamar o método estático spy()
do Mockito e passar uma instância para espionar. Chamar métodos do objeto retornado chamará métodos reais, a menos que esses métodos sejam stub. Essas chamadas são gravadas e os fatos dessas chamadas podem ser verificados (consulte a descrição adicional de verify()
). Vamos fazer um espião:
DecimalFormat decimalFormat = spy(new DecimalFormat()); assertEquals("42", decimalFormat.format(42L));
Criar um espião não difere muito de criar um mock. Além disso, todos os métodos do Mockito usados para configurar uma simulação também são aplicáveis à configuração de um espião.
Spies raramente são usados em comparação com mocks, mas você pode achá-los úteis para testar código legado que não pode ser refatorado, onde testá-lo requer mocking parcial. Nesses casos, você pode simplesmente criar um espião e stub alguns de seus métodos para obter o comportamento desejado.
Valores de retorno padrão
Chamar mock(PasswordEncoder.class)
retorna uma instância de PasswordEncoder
. Podemos até chamar seus métodos, mas o que eles vão retornar? Por padrão, todos os métodos de um mock retornam valores “não inicializados” ou “vazios”, por exemplo, zeros para tipos numéricos (tanto primitivos quanto em caixa), false para booleanos e nulos para a maioria dos outros tipos.
Considere a seguinte interface:
interface Demo { int getInt(); Integer getInteger(); double getDouble(); boolean getBoolean(); String getObject(); Collection<String> getCollection(); String[] getArray(); Stream<?> getStream(); Optional<?> getOptional(); }
Agora considere o seguinte trecho, que dá uma ideia de quais valores padrão esperar dos métodos de uma simulação:
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 stub
Mocks novos e inalterados são úteis apenas em casos raros. Normalmente, queremos configurar o mock e definir o que fazer quando métodos específicos do mock são chamados. Isso é chamado de stub .
Mockito oferece duas maneiras de stub. A primeira maneira é “ quando este método é chamado, então faça alguma coisa”. Considere o seguinte trecho:
when(passwordEncoder.encode("1")).thenReturn("a");
É quase como em inglês: “Quando passwordEncoder.encode(“1”)
é chamado, retorne um a
.”
A segunda maneira de stub é mais como “Faça algo quando o método deste mock for chamado com os seguintes argumentos”. Essa maneira de stub é mais difícil de ler, pois a causa é especificada no final. Considerar:
doReturn("a").when(passwordEncoder).encode("1");
O trecho com este método de stubbing seria: “Retorne a
quando o método encode()
de passwordEncoder
for chamado com um argumento de 1
.”
A primeira maneira é considerada preferida porque é segura para tipos e porque é mais legível. Raramente, no entanto, você é forçado a usar a segunda maneira, como quando tenta enganar um método real de um espião, porque chamá-lo pode ter efeitos colaterais indesejados.
Vamos explorar brevemente os métodos de stubbing fornecidos pelo Mockito. Incluiremos as duas formas de stub em nossos exemplos.
Valores de retorno
thenReturn
ou doReturn()
são usados para especificar um valor a ser retornado na chamada do método.
//”when this method is called, then do something” when(passwordEncoder.encode("1")).thenReturn("a");
ou
//”do something when this mock's method is called with the following arguments” doReturn("a").when(passwordEncoder).encode("1");
Você também pode especificar vários valores que serão retornados como resultados de chamadas de método consecutivas. O último valor será usado como resultado para todas as outras chamadas de método.
//when when(passwordEncoder.encode("1")).thenReturn("a", "b");
ou
//do doReturn("a", "b").when(passwordEncoder).encode("1");
O mesmo pode ser feito com o seguinte trecho:
when(passwordEncoder.encode("1")) .thenReturn("a") .thenReturn("b");
Esse padrão também pode ser usado com outros métodos de stub para definir os resultados de chamadas consecutivas.
Retornando respostas personalizadas
then()
, um alias para thenAnswer()
e doAnswer()
alcançam a mesma coisa, que é configurar uma resposta personalizada a ser retornada quando um método é chamado, assim:
when(passwordEncoder.encode("1")).thenAnswer( invocation -> invocation.getArgument(0) + "!");
ou
doAnswer(invocation -> invocation.getArgument(0) + "!") .when(passwordEncoder).encode("1");
O único argumento que thenAnswer()
aceita é uma implementação da interface Answer
. Possui um único método com um parâmetro do tipo InvocationOnMock
.
Você também pode lançar uma exceção como resultado de uma chamada de método:
when(passwordEncoder.encode("1")).thenAnswer(invocation -> { throw new IllegalArgumentException(); });
…ou chame o método real de uma classe (não aplicável a 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());
Você está certo se você acha que parece complicado. O Mockito fornece thenCallRealMethod()
e thenThrow()
para simplificar esse aspecto de seus testes.
Chamando Métodos Reais
Como o próprio nome sugere, thenCallRealMethod()
e doCallRealMethod()
chamam o método real em um objeto simulado:
Date mock = mock(Date.class); when(mock.getTime()).thenCallRealMethod(); doCallRealMethod().when(mock).setTime(42); mock.setTime(42); assertEquals(42, mock.getTime());
Chamar métodos reais pode ser útil em simulações parciais, mas certifique-se de que o método chamado não tenha efeitos colaterais indesejados e não dependa do estado do objeto. Se isso acontecer, um espião pode ser mais adequado do que uma simulação.
Se você criar um mock de uma interface e tentar configurar um stub para chamar um método real, o Mockito lançará uma exceção com uma mensagem muito informativa. Considere o seguinte trecho:
when(passwordEncoder.encode("1")).thenCallRealMethod();
O Mockito falhará com a seguinte mensagem:
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();
Parabéns aos desenvolvedores do Mockito por se importarem o suficiente para fornecer descrições tão completas!
Lançando exceções
thenThrow()
e doThrow()
configuram um método simulado para lançar uma exceção:
when(passwordEncoder.encode("1")).thenThrow(new IllegalArgumentException());
ou
doThrow(new IllegalArgumentException()).when(passwordEncoder).encode("1");
Mockito garante que a exceção que está sendo lançada é válida para aquele método stub específico e irá reclamar se a exceção não estiver na lista de exceções verificadas do método. Considere o seguinte:
when(passwordEncoder.encode("1")).thenThrow(new IOException());
Isso levará a um erro:
org.mockito.exceptions.base.MockitoException: Checked exception is invalid for this method! Invalid: java.io.IOException
Como você pode ver, Mockito detectou que encode()
não pode lançar um IOException
.
Você também pode passar uma classe de exceção em vez de passar uma instância de uma exceção:
when(passwordEncoder.encode("1")).thenThrow(IllegalArgumentException.class);
ou
doThrow(IllegalArgumentException.class).when(passwordEncoder).encode("1");
Dito isso, o Mockito não pode validar uma classe de exceção da mesma forma que validará uma instância de exceção, portanto, você deve ser disciplinado e não passar objetos de classe ilegais. Por exemplo, o seguinte lançará IOException
, embora não se espere que encode()
lance uma exceção verificada:
when(passwordEncoder.encode("1")).thenThrow(IOException.class); passwordEncoder.encode("1");
Interfaces de simulação com métodos padrão
Vale a pena notar que ao criar um mock para uma interface, o Mockito zomba de todos os métodos dessa interface. Desde o Java 8, as interfaces podem conter métodos padrão junto com métodos abstratos. Esses métodos também são zombados, então você precisa tomar cuidado para que eles funcionem como métodos padrão.
Considere o seguinte exemplo:
interface AnInterface { default boolean isTrue() { return true; } } AnInterface mock = mock(AnInterface.class); assertFalse(mock.isTrue());
Neste exemplo, assertFalse()
será bem-sucedido. Se isso não é o que você esperava, certifique-se de que Mockito chamou o método real, assim:
AnInterface mock = mock(AnInterface.class); when(mock.isTrue()).thenCallRealMethod(); assertTrue(mock.isTrue());
Combinadores de argumentos
Nas seções anteriores, configuramos nossos métodos simulados com valores exatos como argumentos. Nesses casos, o Mockito apenas chama equals()
internamente para verificar se os valores esperados são iguais aos valores reais.
Às vezes, porém, não conhecemos esses valores de antemão.
Talvez não nos importemos com o valor real que está sendo passado como argumento, ou talvez queiramos definir uma reação para uma gama mais ampla de valores. Todos esses cenários (e mais) podem ser resolvidos com correspondências de argumentos. A ideia é simples: em vez de fornecer um valor exato, você fornece um matcher de argumentos para que o Mockito corresponda aos argumentos do método.
Considere o seguinte trecho:
when(passwordEncoder.encode(anyString())).thenReturn("exact"); assertEquals("exact", passwordEncoder.encode("1")); assertEquals("exact", passwordEncoder.encode("abc"));
Você pode ver que o resultado é o mesmo independentemente do valor que passamos para encode()
porque usamos o matcher de argumento anyString()
nessa primeira linha. Se reescrevermos essa linha em inglês simples, soaria como “quando o codificador de senha for solicitado a codificar qualquer string, então retorne a string 'exact'”.
O Mockito exige que você forneça todos os argumentos por correspondências ou por valores exatos. Portanto, se um método tiver mais de um argumento e você quiser usar correspondências de argumento apenas para alguns de seus argumentos, esqueça. Você não pode escrever código assim:
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 corrigir o erro, devemos substituir a última linha para incluir o matcher do argumento eq
para a
, da seguinte forma:
when(mock.call(eq("a"), anyInt())).thenReturn(true);
Aqui usamos os casadores de argumentos eq()
e anyInt()
, mas há muitos outros disponíveis. Para obter uma lista completa de correspondências de argumentos, consulte a documentação na classe org.mockito.ArgumentMatchers
.
É importante observar que você não pode usar correspondências de argumentos fora da verificação ou stub. Por exemplo, você não pode ter o seguinte:
//this won't work String orMatcher = or(eq("a"), endsWith("b")); verify(mock).encode(orMatcher);
O Mockito detectará o matcher de argumento mal colocado e lançará um InvalidUseOfMatchersException
. A verificação com casadores de argumentos deve ser feita desta forma:
verify(mock).encode(or(eq("a"), endsWith("b")));
Os correspondentes de argumento também não podem ser usados como valor de retorno. Mockito não pode retornar anyString()
ou qualquer coisa; um valor exato é necessário ao stub chamadas.
Correspondências personalizadas
Os matchers personalizados são úteis quando você precisa fornecer alguma lógica de correspondência que ainda não esteja disponível no Mockito. A decisão de criar um matcher personalizado não deve ser tomada de ânimo leve, pois a necessidade de combinar argumentos de maneira não trivial indica um problema no design ou que um teste está ficando muito complicado.
Como tal, vale a pena verificar se você pode simplificar um teste usando alguns dos correspondentes de argumentos lenientes, como isNull()
e nullable()
antes de escrever um correspondente personalizado. Se você ainda sentir a necessidade de escrever um matcher de argumentos, o Mockito fornece uma família de métodos para fazer isso.
Considere o seguinte exemplo:
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")));
Aqui criamos o matcher de argumento hasLuck
e usamos argThat()
para passar o matcher como um argumento para um método simulado, stub para retornar true
se o nome do arquivo terminar com “luck”. Você pode tratar ArgumentMatcher
como uma interface funcional e criar sua instância com um lambda (que é o que fizemos no exemplo). Uma sintaxe menos concisa ficaria assim:
ArgumentMatcher<File> hasLuck = new ArgumentMatcher<File>() { @Override public boolean matches(File file) { return file.getName().endsWith("luck"); } };
Se você precisar criar um matcher de argumentos que funcione com tipos primitivos, existem vários outros métodos para isso em org.mockito.ArgumentMatchers
:
- charThat(ArgumentMatcher<Character> matcher)
- booleanThat(ArgumentMatcher<Boolean> matcher)
- byteThat(ArgumentMatcher<Byte> matcher)
- shortThat(ArgumentMatcher<Short> matcher)
- intThat(ArgumentMatcher<Integer> matcher)
- longThat(ArgumentMatcher<Long> matcher)
- floatThat(ArgumentMatcher<Float> matcher)
- doubleThat(ArgumentMatcher<Double> matcher)
Combinando Matchers
Nem sempre vale a pena criar um matcher de argumento personalizado quando uma condição é muito complicada para ser tratada com matchers básicos; às vezes, combinar matchers fará o truque. O Mockito fornece matchers de argumentos para implementar operações lógicas comuns ('not', 'and', 'or') em matchers de argumentos que correspondem a tipos primitivos e não primitivos. Esses correspondentes são implementados como métodos estáticos na classe org.mockito.AdditionalMatchers
.
Considere o seguinte exemplo:
when(passwordEncoder.encode(or(eq("1"), contains("a")))).thenReturn("ok"); assertEquals("ok", passwordEncoder.encode("1")); assertEquals("ok", passwordEncoder.encode("123abc")); assertNull(passwordEncoder.encode("123"));
Aqui nós combinamos os resultados de dois casadores de argumentos: eq("1")
e contains("a")
. A expressão final, or(eq("1"), contains("a"))
, pode ser interpretada como “a string do argumento deve ser igual a “1” ou conter “a”.
Observe que há correspondências menos comuns listadas na classe org.mockito.AdditionalMatchers
, como geq()
, leq()
, gt()
e lt()
, que são comparações de valor aplicáveis a valores primitivos e instâncias de java.lang.Comparable
.

Verificando o comportamento
Uma vez que um mock ou spy foi usado, podemos verify
que interações específicas ocorreram. Literalmente, estamos dizendo “Ei, Mockito, certifique-se de que esse método foi chamado com esses argumentos”.
Considere o seguinte exemplo artificial:
PasswordEncoder passwordEncoder = mock(PasswordEncoder.class); when(passwordEncoder.encode("a")).thenReturn("1"); passwordEncoder.encode("a"); verify(passwordEncoder).encode("a");
Aqui nós configuramos uma simulação e chamamos seu método encode()
. A última linha verifica se o método encode()
do mock foi chamado com o valor de argumento específico a
. Observe que a verificação de uma invocação stub é redundante; o objetivo do trecho anterior é mostrar a ideia de fazer a verificação depois que algumas interações aconteceram.
Se alterarmos a última linha para ter um argumento diferente - digamos, b
- o teste anterior falhará e o Mockito reclamará que a invocação real tem argumentos diferentes ( b
em vez do esperado a
).
Os correspondentes de argumento podem ser usados para verificação, assim como para stub:
verify(passwordEncoder).encode(anyString());
Por padrão, o Mockito verifica se o método foi chamado uma vez, mas você pode verificar qualquer número de invocações:
// 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());
Um recurso raramente usado de verify()
é sua capacidade de falhar em um tempo limite, o que é útil principalmente para testar código simultâneo. Por exemplo, se nosso codificador de senha for chamado em outro thread simultaneamente com verify()
, podemos escrever um teste da seguinte forma:
usePasswordEncoderInOtherThread(); verify(passwordEncoder, timeout(500)).encode("a");
Este teste será bem-sucedido se encode()
for chamado e concluído em 500 milissegundos ou menos. Se você precisar esperar o período completo especificado, use after()
em vez de timeout()
:
verify(passwordEncoder, after(500)).encode("a");
Outros modos de verificação ( times()
, atLeast()
, etc) podem ser combinados com timeout()
e after()
para fazer testes mais complicados:
// passes as soon as encode() has been called 3 times within 500 ms verify(passwordEncoder, timeout(500).times(3)).encode("a");
Além de times()
, os modos de verificação suportados incluem only()
, atLeast()
e atLeastOnce()
(como um alias para atLeast(1)
).
O Mockito também permite verificar a ordem de chamada em um grupo de mocks. Não é um recurso para ser usado com muita frequência, mas pode ser útil se a ordem das invocações for importante. Considere o seguinte exemplo:
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");
Se reorganizarmos a ordem das chamadas simuladas, o teste falhará com VerificationInOrderFailure
.
A ausência de invocações também pode ser verificada usando verifyZeroInteractions()
. Este método aceita um mock ou mocks como um argumento e falhará se algum método do(s) mock(s) passado(s) for chamado.
Também vale a pena mencionar o método verifyNoMoreInteractions()
, pois ele recebe mocks como argumento e pode ser usado para verificar se todas as chamadas desses mocks foram verificadas.
Capturando argumentos
Além de verificar se um método foi chamado com argumentos específicos, o Mockito permite que você capture esses argumentos para depois executar asserções personalizadas neles. Em outras palavras, você está dizendo “Ei, Mockito, verifique se esse método foi chamado e me dê os valores de argumento com os quais foi chamado”.
Vamos criar uma simulação de PasswordEncoder
, chamar encode()
, capturar o argumento e verificar seu 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 você pode ver, passamos passwordCaptor.capture()
como um argumento de encode()
para verificação; isso cria internamente um matcher de argumento que salva o argumento. Em seguida, recuperamos o valor capturado com passwordCaptor.getValue()
e o inspecionamos com assertEquals()
.
Se precisarmos capturar um argumento em várias chamadas, o ArgumentCaptor
permite que você recupere todos os valores com getAllValues()
, assim:
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());
A mesma técnica pode ser usada para capturar argumentos do método de aridade variável (também conhecido como varargs).
Testando nosso exemplo simples
Agora que sabemos muito mais sobre o Mockito, é hora de voltar à nossa demonstração. Vamos escrever o teste do método isValidUser
. Veja como pode ser:
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; } }
Mergulhando sob a API
O Mockito fornece uma API legível e conveniente, mas vamos explorar alguns de seus funcionamentos internos para entender suas limitações e evitar erros estranhos.
Vamos examinar o que está acontecendo dentro do Mockito quando o seguinte snippet é executado:
// 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, a primeira linha cria uma simulação. Mockito usa ByteBuddy para criar uma subclasse da classe dada. O novo objeto de classe tem um nome gerado como demo.mockito.PasswordEncoder$MockitoMock$1953422997
, seu equals()
atuará como verificação de identidade e hashCode()
retornará um código hash de identidade. Uma vez que a classe é gerada e carregada, sua instância é criada usando Objenesis.
Vejamos a próxima linha:
when(mock.encode("a")).thenReturn("1");
A ordenação é importante: A primeira instrução executada aqui é mock.encode("a")
, que invocará encode()
no mock com um valor de retorno padrão de null
. Então, realmente, estamos passando null
como um argumento de when()
. O Mockito não se importa com o valor exato que está sendo passado para when()
porque ele armazena informações sobre a invocação de um método simulado no chamado 'stubbing contínuo' quando esse método é invocado. Mais tarde, quando estamos chamando when()
, Mockito puxa esse objeto de stubbing em andamento e o retorna como resultado de when()
. Em seguida, chamamos thenReturn(“1”)
no objeto de stub em andamento retornado.
A terceira linha, mock.encode("a");
é simples: estamos chamando o método stubbed. Internamente, o Mockito salva essa invocação para verificação adicional e retorna a resposta de invocação stub; no nosso caso, é a string 1
.
Na quarta linha ( verify(mock).encode(or(eq("a"), endsWith("b")));
), estamos pedindo ao Mockito para verificar se houve uma invocação de encode()
com aqueles argumentos específicos.
verify()
é executado primeiro, o que transforma o estado interno do Mockito em modo de verificação. É importante entender que o Mockito mantém seu estado em um ThreadLocal
. Isso torna possível implementar uma sintaxe legal, mas, por outro lado, pode levar a um comportamento estranho se o framework for usado de forma inadequada (se você tentar usar matchers de argumentos fora da verificação ou stub, por exemplo).
Então, como Mockito cria um or
matcher? Primeiro, eq("a")
é chamado e um equals
matcher é adicionado à pilha de matchers. Em segundo lugar, endsWith("b")
é chamado e um matcher endsWith
é adicionado à pilha. Por fim, or(null, null)
é chamado—ele usa os dois matchers que retira da pilha, cria o matcher or
e o empurra para a pilha. Finalmente, encode()
é chamado. Mockito então verifica se o método foi invocado o número esperado de vezes e com os argumentos esperados.
Embora os correspondentes de argumento não possam ser extraídos para variáveis (porque altera a ordem de chamada), eles podem ser extraídos para métodos. Isso preserva a ordem de chamada e mantém a pilha no estado correto:
verify(mock).encode(matchCondition()); … String matchCondition() { return or(eq("a"), endsWith("b")); }
Alterando as respostas padrão
Nas seções anteriores, criamos nossos mocks de forma que, quando qualquer método mocked é chamado, eles retornam um valor “vazio”. Esse comportamento é configurável. Você pode até mesmo fornecer sua própria implementação de org.mockito.stubbing.Answer
se os fornecidos pelo Mockito não forem adequados, mas pode ser uma indicação de que algo está errado quando os testes de unidade ficam muito complicados. Lembre-se do princípio KISS!
Vamos explorar a oferta de respostas padrão predefinidas do Mockito:
RETURNS_DEFAULTS
é a estratégia padrão; não vale a pena mencionar explicitamente ao configurar uma simulação.CALLS_REAL_METHODS
faz com que invocações sem tubulação chamem métodos reais.RETURNS_SMART_NULLS
evita umNullPointerException
retornandoSmartNull
em vez denull
ao usar um objeto retornado por uma chamada de método sem tubulações. Você ainda falhará com umNullPointerException
, masSmartNull
oferece um rastreamento de pilha mais agradável com a linha em que o método untubbed foi chamado. Isso faz com que valha a pena terRETURNS_SMART_NULLS
como a resposta padrão no Mockito!RETURNS_MOCKS
primeiro tenta retornar valores “vazios” comuns, depois simula, se possível, enull
caso contrário. O critério de vazio difere um pouco do que vimos anteriormente: Em vez de retornarnull
para strings e arrays, os mocks criados comRETURNS_MOCKS
retornam strings e arrays vazios, respectivamente.RETURNS_SELF
é útil para simular construtores. Com essa configuração, um mock retornará uma instância de si mesmo se um método for chamado que retorne algo de um tipo igual à classe (ou uma superclasse) da classe mocked.RETURNS_DEEP_STUBS
vai mais fundo queRETURNS_MOCKS
e cria mocks que são capazes de retornarnull
deRETURNS_DEEP_STUBS
deRETURNS_MOCKS
, etc.
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());
Nomeando uma simulação
O Mockito permite que você nomeie um mock, um recurso útil se você tiver muitos mocks em um teste e precisar distingui-los. Dito isso, a necessidade de nomear mocks pode ser um sintoma de design ruim. Considere o seguinte:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class); verify(robustPasswordEncoder).encode(anyString());
Mockito vai reclamar, mas como não nomeamos formalmente os mocks, não sabemos qual:
Wanted but not invoked: passwordEncoder.encode(<any string>);
Vamos nomeá-los passando uma string na construção:
PasswordEncoder robustPasswordEncoder = mock(PasswordEncoder.class, "robustPasswordEncoder"); PasswordEncoder weakPasswordEncoder = mock(PasswordEncoder.class, "weakPasswordEncoder"); verify(robustPasswordEncoder).encode(anyString());
Agora a mensagem de erro é mais amigável e aponta claramente para robustPasswordEncoder
:
Wanted but not invoked: robustPasswordEncoder.encode(<any string>);
Implementando várias 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 o seguinte exemplo:
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.
Outros 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.