Criando Spies com spyk no MockK
Ao concluirmos esta aula, nós:
- Compreenderemos qual é a diferença fundamental entre um Mock e um Spy criado com uma biblioteca de mocking.
- Saberemos como criar Spies utilizando
spyk()no MockK (eMockito.spy()no Mockito). - Seremos capazes de substituir seletivamente o comportamento de métodos (partial stubbing) e verificar interações ocorridas em um Spy.
[!NOTE] Esta aula assume que:
Um Spy Não É um Mock!
Antes de criarmos spies com o MockK (ou com o Mockito), precisamos compreender que os spies criados por bibliotecas de mocking modernas são um pouco diferentes tanto dos spies manuais tradicionais que discutimos anteriormente quanto dos objetos mock puros. A melhor maneira de demonstrar essa diferença é comparar diretamente como um Mock e um Spy funcionam por baixo dos panos:
- Primeiro, quando criamos um Mock (
mockk<T>()/Mockito.mock()): A biblioteca de mocking não executa o código real da classe. Ela cria uma casca (proxy) com a mesma API do objeto real, mas jamais invoca os métodos reais. Em vez disso, nosso objeto mock retorna apenas a resposta que configuramos comevery { ... } returns ...(ou a resposta padrão caso seja relaxado).
- Segundo, quando criamos um Spy (
spyk(realObject)/Mockito.spy(realObject)): A biblioteca cria um wrapper que possui a mesma API do objeto real e que delega todas as chamadas de método para o objeto real envolvido. Em outras palavras, quando invocamos um método de um spy, o spy grava as informações dessa invocação no histórico e, em seguida, executa o método verdadeiro do objeto real!
Quando falamos sobre spies manuais clássicos no Tópico 04, dissemos que um spy puro não substituía respostas de métodos. Porém, quando criamos um spy com o MockK (spyk) ou Mockito (spy), podemos fazer stubbing seletivo de métodos específicos do spy! Se configurarmos o retorno de um método específico em um spy, o spy não invocará o método real quando aquele método stubado for chamado pelo Sistema Sob Teste (enquanto todos os demais métodos continuarão executando o código real).
Essa é uma técnica poderosa (embora deva ser usada com parcimônia) quando precisamos lidar com código legado que não foi originalmente projetado para ser testado.
Vamos conhecer a classe real que envolveremos com nosso spy nesta aula.
Introdução ao Sistema Sob Teste (SUT)
Durante esta aula, criaremos um spy que envolve uma classe concreta que implementa a interface TaskRepository. O nome dessa classe é TaskRepositoryImpl, e cada método da interface TaskRepository é implementado seguindo estes dois requisitos:
- O método registra no log o nome do método invocado e seus parâmetros.
- O método retorna uma resposta vazia (
0L,Optional.empty()ounull).
O código-fonte da classe TaskRepositoryImpl em Kotlin é o seguinte:
|
|
Vamos descobrir como podemos criar um spy que envolve um objeto TaskRepositoryImpl utilizando o MockK!
Criando Spies com o MockK (spyk)
O MockK fornece duas sobrecargas principais da função spyk() (equivalentes aos dois métodos Mockito.spy() do Mockito):
spyk<T>()(sem argumentos): Cria um spy instanciando automaticamente a classeTatravés do seu construtor padrão sem argumentos (ou criando uma instância de uma classe abstrata).spyk(objectToSpy)(recebendo uma instância): Cria um spy que envolve o objetoobjectToSpyexistente passado como parâmetro.
Como sabemos, os dublês de teste criados com o MockK não são stateless. Por isso, devemos criar nosso spy antes que cada método de teste seja invocado, dentro de um método @BeforeEach.
Para criar um spy que envolve uma instância real de TaskRepositoryImpl, invocamos spyk(TaskRepositoryImpl()) (e não precisamos adicionar nenhum método em TestDoubles, pois o próprio nome spyk já deixa óbvio que estamos criando um spy).
Depois de fazermos isso, o código-fonte inicial da nossa classe de teste fica assim:
|
|
Depois de criarmos o nosso spy, podemos tanto substituir o comportamento de métodos específicos (stubbing) quanto verificar as interações ocorridas entre o Sistema Sob Teste e o nosso spy. Vamos começar descobrindo como fazer o stubbing de um método de um spy!
Configurando o Comportamento de um Método em um Spy
Aqui novamente vemos uma grande vantagem de design do MockK em relação ao Mockito clássico:
No Mockito clássico, quando você queria fazer stubbing de um método em um spy, você não podia usar a sintaxe normal given(repository.findById(1L)).willReturn(Optional.of(expected)) nem when(repository.findById(1L)).thenReturn(...)! Se você usasse a sintaxe normal no Mockito, o método real TaskRepositoryImpl.findById(1L) seria executado de verdade durante a linha de configuração antes de o Mockito interceptá-lo (imprimindo o log real ou até abrindo conexões reais!). Por isso, no Mockito era obrigatório usar a sintaxe invertida:
BDDMockito:willReturn(Optional.of(expected)).given(repository).findById(1L);Mockito:doReturn(Optional.of(expected)).when(repository).findById(1L);
No MockK, o bloco every { ... } returns ... intercepta a chamada antes que o método real do spy seja invocado! Portanto, usamos exatamente a mesma sintaxe padrão every { repository.findById(1L) } returns Optional.of(expected)!
Veja como fica o método de teste que faz o stubbing do método findById(1L) em nosso spy:
|
|
Quando executamos esse método de teste, notamos que o método real findById() da classe TaskRepositoryImpl não foi executado (nenhuma mensagem "Invoked findById() method with id: 1" aparece no log), e o spy retornou exatamente o Optional.of(expected) configurado! Enquanto isso, se chamarmos repository.count() (que não foi stubado), o spy executará o método real da classe TaskRepositoryImpl e imprimirá "Invoked count() method".
Vamos avançar e ver como verificar interações ocorridas com o nosso spy.
Verificando Interações em um Spy com o MockK
Podemos verificar as interações que aconteceram (ou não aconteceram) entre o Sistema Sob Teste e o nosso spy utilizando exatamente a mesma sintaxe verify { ... } que aprendemos na aula anterior (Criando Mocks e Verificando Interações com verify).
Por exemplo, se quisermos verificar que o método findById() da interface TaskRepository foi invocado uma vez no nosso spy e que 1L foi passado como parâmetro, escrevemos o seguinte teste unitário:
|
|
Quando rodamos esse teste, o spy executa o método real TaskRepositoryImpl.findById(1L) (imprimindo a linha no log) e grava a chamada, permitindo que verify(exactly = 1) { repository.findById(1L) } confirme que a interação ocorreu com sucesso!
Executando Nossos Testes no Gradle
Ao executarmos a suíte de testes de Spies através do Gradle (./gradlew test --tests 'com.testwithspring.starter.mockk.MockKSpy*' -i), inspecionamos a evidência real de partial stubbing e verificação de interações:
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.MockKSpyStubTest > deve retornar objeto configurado sem executar o metodo real do spy PASSED
com.testwithspring.starter.mockk.MockKSpyVerifyTest > deve verificar quando o metodo do spy foi chamado uma vez PASSED
06 - Criando Spies com spyk no MockK > 2 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.008 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.015 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 quatro pontos essenciais:
- Um Spy criado com o MockK (
spyk) ou Mockito (spy) envolve um objeto real e invoca seus métodos verdadeiros, a menos que tenhamos feito o stubbing explícito do método chamado. - Criamos spies no MockK utilizando a função
spyk(realObject)(ouMockito.spy(realObject)no Mockito). - No Mockito clássico, para fazer stubbing de um método de um spy sem disparar o método real, é obrigatório usar a sintaxe invertida (
willReturn(...).given(spy)/doReturn(...).when(spy)). No MockK, o blocoevery { spy.method() } returns ...já evita a execução do método real automaticamente. - Utilizamos exatamente a mesma sintaxe (
verify { ... }) para verificar interações ocorridas entre o Sistema Sob Teste e nosso dublê de teste, seja ele um Mock ou um Spy.