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:
|
|
Executando Nossos Testes no Gradle
Ao executarmos a suíte MockKTaskServiceVerifyTest através do Gradle (./gradlew test --tests MockKTaskServiceVerifyTest -i), comprovamos a captura do argumento com slot e a verificação do Mock com verify:
Initialized native services in: /home/wc/.gradle/native
Initialized jansi services in: /home/wc/.gradle/native
The client will now receive all logging from the daemon (pid: 16102, address: [fe80::1%1:54330]).
Gradle Daemon started in 1 s 042 ms
Starting Build
Settings evaluated using settings file '/home/wc/test-with-spring/code/01-starter-junit6/settings.gradle.kts'.
Projects loaded. Root project using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Included projects: [root project 'starter-junit6']
> Configure project :
Evaluating root project 'starter-junit6' using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Using Kotlin Gradle Plugin 2.4.21
Using Java Toolchain: Temurin 21.0.6 (/home/wc/.sdkman/candidates/java/21.0.6-tem)
Spring Boot 4.1.2 plugin applied.
All projects evaluated.
Selected primary task 'test' from project :
Tasks to be executed: [task ':compileKotlin', task ':compileJava', task ':processResources', task ':classes', task ':compileTestKotlin', task ':compileTestJava', task ':processTestResources', task ':testClasses', task ':test']
Tasks that were skipped: [task ':compileJava', task ':compileTestJava']
> Task :compileKotlin UP-TO-DATE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :compileTestKotlin UP-TO-DATE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test
Starting process 'Gradle Test Executor 1'. Working directory: /home/wc/test-with-spring/code/01-starter-junit6 Command: /home/wc/.sdkman/candidates/java/21.0.6-tem/bin/java -ea -cp ...
Successfully started process 'Gradle Test Executor 1'
com.testwithspring.starter.mockk.MockKVerifyTest > deve verificar quando o metodo foi chamado uma vez PASSED
com.testwithspring.starter.mockk.MockKVerifyTest > deve verificar que o metodo foi chamado exatamente uma vez PASSED
com.testwithspring.starter.mockk.MockKVerifyTest > deve verificar que o metodo nunca foi invocado PASSED
com.testwithspring.starter.mockk.MockKVerifyTest > deve verificar que nenhum outro metodo foi invocado PASSED
com.testwithspring.starter.mockk.MockKVerifyTest > deve verificar que nenhuma interacao ocorreu entre o SUT e o mock PASSED
com.testwithspring.starter.mockk.MockKVerifyTest > deve verificar que o metodo foi invocado passando 1L com eq PASSED
com.testwithspring.starter.mockk.MockKVerifyTest > deve verificar que o metodo foi invocado passando qualquer Long com any PASSED
com.testwithspring.starter.mockk.MockKVerifyTest > deve verificar que o metodo foi invocado capturando o argumento com slot PASSED
com.testwithspring.starter.mockk.MockKTaskServiceVerifyTest > createCopyOf quando tarefa de origem for encontrada deve salvar nova tarefa PASSED
05 - Criando Mocks e Verificando Interações com verify > 9 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.014 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/test-results/test
Generating HTML test report...
Finished generating test html results (0.021 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/reports/tests/test
BUILD SUCCESSFUL in 1s
5 actionable tasks: 1 executed, 4 up-to-date
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).