Criando Mocks e Verificando Interações com verify
Ao concluirmos esta aula, nós:
- Compreenderemos como criar nossos objetos Mock com o MockK.
- Seremos capazes de verificar as interações que aconteceram (ou não aconteceram) entre o Sistema Sob Teste e um objeto Mock (
verify,confirmVerified,Called). - Saberemos como capturar o parâmetro real passado para um método invocado utilizando
slot<T>()(CapturingSlot, o equivalente aoArgumentCaptor<T>do Mockito).
[!NOTE] Esta aula assume que:
Criando Mocks com o MockK
Como vimos nas aulas anteriores, podemos criar um dummy, um stub ou um mock invocando a função mockk() (ou Mockito.mock()).
Além disso, como os dublês de teste criados com o MockK não são stateless, devemos criar nossos dublês de teste antes que cada método de teste seja invocado, utilizando um método de setup anotado com @BeforeEach.
Desta vez, não precisamos adicionar nenhum método fábrica novo à classe TestDoubles, pois já é 100% cristalino que a função mockk() (ou mock()) cria um objeto Mock!
Depois de criarmos nosso objeto mock em um método @BeforeEach, o código-fonte inicial da nossa classe de teste fica assim:
|
|
[!NOTE] Mocks Estritos (
strictMock) vs. Mocks Relaxados No Mockito clássico,Mockito.mock(TaskRepository::class.java)cria um mock relaxado por padrão; se você quisesse um strict mock no Mockito (que lança exceção ao receber chamadas não configuradas), adicionava um métodostrictMock()emTestDoubles:
1 2 3object TestDoubles { inline fun <reified T : Any> strictMock(): T = mockk<T>(relaxed = false) }No MockK,
mockk<TaskRepository>()sem argumentos já é estrito por padrão, enquantomockk<TaskRepository>(relaxed = true)permite invocar métodos sem configuração prévia deevery { ... }quando queremos focar exclusivamente na verificação posterior comverify { ... }.
Vamos descobrir como podemos verificar interações com o MockK!
Verificando Interações com o MockK (e o Paralelo com o Mockito)
O MockK fornece funções equivalentes aos três métodos estáticos clássicos de verificação do Mockito:
verify { ... }(equivalente aMockito.verify()): Verifica se uma interação específica aconteceu (ou quantas vezes aconteceu) entre o Sistema Sob Teste e o nosso objeto mock.confirmVerified(mock)(equivalente aMockito.verifyNoMoreInteractions(mock)): Faz o teste falhar se qualquer um dos objetos mock passados como parâmetro possuir interações que não foram verificadas por uma chamada anterior deverify { ... }. Embora possa ser útil em situações pontuais, não devemos usarconfirmVerified()indiscriminadamente em todos os métodos de teste para não torná-los frágeis.verify { mock wasNot Called }(equivalente aMockito.verifyZeroInteractions(mock)/verifyNoInteractions(mock)): Verifica que nenhuma interação aconteceu entre o Sistema Sob Teste e o objeto mock especificado.
A seguir, veremos na prática como utilizar essas funções em nossos testes unitários!
Verificando Invocações de Métodos: 5 Exemplos Práticos
Os próximos cinco exemplos demonstram como verificar interações que aconteceram (ou que não aconteceram) entre o Sistema Sob Teste e o nosso objeto mock.
Exemplo 1: Verificar que o Método Foi Invocado Pelo Menos Uma Vez (ou Uma Vez)
Para verificar que o Sistema Sob Teste invocou o método findById() da interface TaskRepository passando 1L como parâmetro:
- Invocamos a função
verify { ... }do MockK (ouverify(exactly = 1) { ... }). - Dentro do bloco lambda de
verify, fazemos a chamada ao métodorepository.findById(1L)com o parâmetro esperado.
(No Mockito, escrevíamos verify(repository).findById(1L)).
O código-fonte do nosso teste unitário fica assim:
|
|
Exemplo 2: Verificar que o Método Foi Invocado Exatamente X Vezes (exactly = X)
Se quisermos verificar que o Sistema Sob Teste invocou um método do nosso objeto mock exatamente X vezes:
- No Mockito, passávamos um
VerificationModecomo segundo argumento deverify:verify(repository, times(1)).findById(1L)(além deatLeast(n),atLeastOnce(),atMost(n)). - No MockK, passamos os parâmetros nomeados diretamente para a função
verify:verify(exactly = 1) { ... }(equivalente atimes(1))verify(atLeast = 1) { ... }(equivalente aatLeast(1)/atLeastOnce())verify(atMost = 2) { ... }(equivalente aatMost(2))
Veja como fica o teste garantindo que findById(1L) foi invocado exatamente 1 vez:
|
|
Exemplo 3: Verificar que o Método Nunca Foi Invocado (exactly = 0)
Se quisermos verificar que um método específico do nosso objeto mock não foi invocado pelo Sistema Sob Teste:
- No Mockito, usávamos
verify(repository, never()).findById(1L). - No MockK, usamos
verify(exactly = 0) { repository.findById(1L) }!
Se quisermos garantir que o método findById() da interface TaskRepository jamais foi invocado com o parâmetro 1L, escrevemos o seguinte teste unitário:
|
|
Exemplo 4: Verificar que Não Há Interações Não Verificadas (confirmVerified)
Se quisermos verificar que o nosso mock não recebeu nenhuma outra chamada além daquelas que já verificamos explicitamente, seguimos estes dois passos:
- Especificamos as interações esperadas com
verify { ... }. - Invocamos a função
confirmVerified(repository)do MockK (equivalente averifyNoMoreInteractions(repository)do Mockito) passando o mockTaskRepositorycomo parâmetro.
Por exemplo, para verificar que o Sistema Sob Teste invocou findById(1L) uma vez e não invocou nenhum outro método da interface TaskRepository:
|
|
Exemplo 5: Verificar que Não Houve Nenhuma Interação com o Mock (wasNot Called)
Se quisermos verificar que o Sistema Sob Teste não invocou absolutamente nenhum método da interface TaskRepository, usamos verify { repository wasNot Called } (ou confirmVerified(repository) sem verify anterior, equivalente ao verifyZeroInteractions(repository) do Mockito):
|
|
Utilizando Argument Matchers na Verificação
Assim como vimos ao configurar stubs, quando passamos um valor diretamente para o método dentro de verify { repository.findById(1L) }, o MockK compara o argumento esperado com o argumento real utilizando o método equals(). Quando precisamos de flexibilidade adicional, usamos Argument Matchers:
Primeiro Exemplo: Usando eq()
Se quisermos garantir que o método findById() foi invocado com um parâmetro igual a 1L usando o matcher eq(1L):
|
|
Segundo Exemplo: Usando any()
Se quisermos verificar que o método findById() foi invocado passando qualquer valor Long como parâmetro, usamos o matcher any() (equivalente a anyLong() no Mockito):
|
|
Capturando Argumentos Reais com CapturingSlot (slot<T>())
Se quisermos escrever asserções detalhadas (com o AssertJ) sobre o parâmetro real que o Sistema Sob Teste passou para o nosso mock, os argument matchers simples (eq, any) não bastam — precisamos capturar o parâmetro real antes de escrever nossas asserções sobre ele!
- No Mockito, capturamos o parâmetro utilizando a classe
ArgumentCaptor<T>(ArgumentCaptor.forClass(T::class.java),argument.capture()eargument.value). - No MockK, capturamos o parâmetro utilizando um
CapturingSlot<T>, criado com a funçãoslot<T>()!
Por exemplo, se quisermos capturar o parâmetro passado para o método findById() da interface TaskRepository e verificar que ele é igual a 1L, seguimos estes quatro passos:
- Criamos um objeto
CapturingSlot<Long>invocando a funçãoslot<Long>()do pacoteio.mockk.slot. - Garantimos que o método
findById()foi invocado uma vez e capturamos o parâmetro passado invocandocapture(argument)dentro deverify. - Obtemos o parâmetro real capturado acessando a propriedade
argument.captured(equivalente aargument.getValue()noArgumentCaptordo Mockito). - Escrevemos uma asserção com o AssertJ garantindo que o valor capturado é igual a
1L.
Após concluirmos esses passos, o código-fonte do nosso método de teste fica assim:
|
|
Agora já sabemos como verificar interações e capturar argumentos entre o Sistema Sob Teste e nosso objeto mock. Vamos colocar nossas novas habilidades em prática!
Colocando a Teoria em Prática
Vamos usar nossas novas habilidades para escrever um teste unitário para o método createCopyOf() da classe TaskService (que conhecemos na aula sobre Stubs). Desta vez, precisamos escrever um teste unitário que garanta que o objeto Task salvo no banco de dados (repository.save(...)) contém as informações corretas.
Podemos escrever esse teste unitário seguindo estes quatro passos:
- Criamos um novo objeto
slot<Task>()(equivalente aArgumentCaptor<Task>). - Garantimos que o método
save()da interfaceTaskRepositoryfoi invocado uma vez e capturamos o objetoTaskreal passado como parâmetro comcapture(taskArgument). - Obtemos o objeto
Taskcapturado comtaskArgument.captured. - Verificamos com o AssertJ se o objeto
Tasksalvo contém o ID do criador, a descrição e o título corretos!
Após finalizarmos nosso teste unitário, o código-fonte completo fica assim:
|
|
Resumo
Nesta aula, aprendemos seis pontos fundamentais:
- Devemos criar nosso objeto mock antes que cada método de teste seja invocado (
@BeforeEach). - Criamos nosso objeto mock utilizando a função
mockk()(ouMockito.mock()). - Verificamos as interações que aconteceram (ou não aconteceram) entre o Sistema Sob Teste e um objeto mock utilizando
verify { ... },verify(exactly = N) { ... },confirmVerified(mock)everify { mock wasNot Called }. - Por padrão, o MockK compara o parâmetro esperado com o parâmetro real utilizando o método
equals(). - Podemos flexibilizar a verificação dos parâmetros utilizando argument matchers (
eq(),any()). - Se precisarmos obter uma referência para o objeto real que foi passado como parâmetro para o método do mock a fim de inspecioná-lo com o AssertJ, utilizamos
slot<T>()+capture(slot)(o equivalente aoArgumentCaptor<T>do Mockito).