Comportamento Estrito vs Relaxado de Stubs no MockK
Ao concluirmos esta aula, nós:
- Saberemos o que é um Strict Stub (Stub Estrito).
- Compreenderemos como criar Strict Stubs tanto no MockK quanto no Mockito.
- Seremos capazes de configurar um Strict Stub para retornar um objeto, lançar uma exceção ou executar uma resposta dinâmica.
- Compreenderemos quando devemos usar Strict Stubs e como o MockK eliminou a principal desvantagem sintática que existia no Mockito clássico.
[!NOTE] Esta aula assume que:
O Que É e Como Criar um Strict Stub?
Antes de criarmos um strict stub, precisamos definir com exatidão o que esse termo significa:
Um Strict Stub (Stub Estrito) é um stub que faz o teste falhar imediatamente caso o Sistema Sob Teste invoque um método inesperado.
Em outras palavras, se o Sistema Sob Teste invocar qualquer método cujo comportamento não tenha sido explicitamente configurado para aquele teste (ou invocá-lo com argumentos diferentes dos configurados), um strict stub deve abortar o teste lançando uma exceção!
Vamos ver como isso funciona no Mockito clássico e como o MockK tornou os strict stubs cidadãos de primeira classe em Kotlin:
1. Como um Strict Stub é criado no Mockito vs. MockK
No Mockito clássico, um dublê criado com Mockito.mock(stubClass) é relaxado por padrão (ele retorna null, 0, false ou coleções vazias quando um método não configurado é chamado). Para criar um strictStub no Mockito, é necessário sobrescrever o Answer padrão passando uma implementação que lança RuntimeException("SUT invoked an unexpected method of a stub"):
|
|
Já no MockK, os criadores da biblioteca perceberam que falhar rapidamente diante de chamadas não configuradas evita bugs silenciosos. Por isso, no MockK, mockk<T>() (com relaxed = false, que é o padrão!) JÁ É um Strict Stub nativo! Se o Sistema Sob Teste chamar qualquer método não configurado em um mockk<T>(), o MockK lança automaticamente um MockKException: no answer found for: ....
Vamos adicionar o método fábrica strictStub() ao nosso objeto utilitário TestDoubles para deixar essa distinção 100% explícita em nosso código:
- Adicionamos a função
strictStub()ao objetoTestDoubles. - Implementamos
strictStub()invocandomockk<T>()no modo estrito padrão (relaxed = false).
Após implementarmos strictStub(), o código-fonte do objeto TestDoubles fica assim:
|
|
Agora podemos criar strict stubs usando nosso novo método fábrica strictStub(). Vamos ver como configuramos o comportamento de métodos em um strict stub!
Configurando o Comportamento de um Strict Stub
Primeiro, criamos nosso strict stub antes que cada método de teste seja invocado, utilizando um método anotado com @BeforeEach:
|
|
A Grande Diferença entre o Mockito e o MockK ao Configurar um Strict Stub
No Mockito, quando você cria um strict stub (cujo Answer padrão lança exceção), você não pode usar a sintaxe normal given(repository.findById(1L)).willReturn(...) nem when(repository.findById(1L)).thenReturn(...)!
Por quê? Porque, em Java, o compilador avalia o argumento repository.findById(1L) antes de chamar given(...) ou when(...). Como o repository é um strict stub ainda não configurado naquele instante, a própria linha de configuração lança RuntimeException("SUT invoked an unexpected method of a stub")!
Por isso, no Mockito, configurar um strict stub obrigava o desenvolvedor a escrever todas as configurações na ordem inversa:
BDDMockito:willReturn(Optional.of(expected)).given(repository).findById(1L);Mockito:doReturn(Optional.of(expected)).when(repository).findById(1L);
No MockK, esse problema não existe! Como every { repository.findById(1L) } recebe um bloco lambda que é executado dentro do contexto de gravação do MockK (e não avaliado ansiosamente antes da função every), utilizamos exatamente a mesma sintaxe natural every { ... } returns ... tanto para stubs relaxados quanto para strict stubs!
Vamos demonstrar isso com os três cenários de configuração:
1. Retornando Valores e Objetos em um Strict Stub
Para configurar o método findById() da interface TaskRepository em nosso strictStub para retornar um Optional não vazio quando for invocado com 1L:
|
|
E o que acontece se alguém chamar repository.findById(2L) ou repository.count() nesse teste? Como repository foi criado com strictStub(), o teste falha imediatamente informando que uma chamada não configurada ocorreu!
2. Lançando uma Exceção em um Strict Stub
Para configurar um método do nosso strictStub para lançar uma exceção:
|
|
[!CAUTION] Lembrando novamente o aviso da aula anterior: o método
findById()retornaOptional<Task>e não lançaNotFoundExceptionpor contrato. Usamos este exemplo apenas para ilustrar a mecânica dethrowsem um strict stub; nunca quebre o contrato da sua API em testes reais!
3. Configurando um answers Dinâmico em um Strict Stub
Para configurar uma resposta dinâmica que inspeciona o argumento 1L em nosso strictStub, inspecionamos a suíte completa StrictStubTest:
|
|
Executando Nossos Testes no Gradle
Ao executarmos a suíte StrictStubTest através do Gradle (./gradlew test --tests StrictStubTest -i), inspecionamos a evidência real de validação rigorosa dos Stubs Estritos:
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.StrictStubTest > deve retornar objeto configurado no strict stub PASSED
com.testwithspring.starter.mockk.StrictStubTest > deve lancar excecao configurada no strict stub PASSED
com.testwithspring.starter.mockk.StrictStubTest > deve retornar objeto usando answers no strict stub PASSED
04 - Comportamento Estrito vs Relaxado de Stubs no MockK > 3 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.009 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.016 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
Devemos Usar Strict Stubs?
Os strict stubs possuem um grande benefício: eles garantem que o nosso teste falhará imediatamente se o Sistema Sob Teste invocar um método cujo comportamento não foi configurado. Embora o valor “vazio” retornado por um stub relaxado geralmente também acabe falhando o teste mais adiante, com um stub relaxado não temos 100% de garantia de que uma chamada inesperada será detectada na hora.
No Mockito clássico, havia um dilema difícil entre duas frases:
- “Quando X for invocado com a entrada Y, retorne Z” (
given(X).willReturn(Z)— sintaxe dos stubs normais no Mockito). - “Retorne Z quando X for invocado com a entrada Y” (
willReturn(Z).given(X)— sintaxe invertida exigida pelos strict stubs no Mockito).
No Mockito, quem preferia a leitura natural da primeira frase acabava abrindo mão dos strict stubs para não ter que escrever willReturn(...).given(...) ao contrário.
Já no MockK, como every { X } returns Z mantém a ordem natural da primeira frase mesmo em stubs estritos (relaxed = false), você tem o melhor dos dois mundos:
- Use
strictStub()(mockk<T>(relaxed = false)) quando quiser máxima segurança e rigor sobre quais métodos de consulta o Sistema Sob Teste pode invocar, sabendo que qualquer chamada não configurada será barrada imediatamente pelo MockK sem sacrificar a legibilidade doevery { ... } returns .... - Use
stub()relaxado (mockk<T>(relaxed = true)) quando o colaborador possuir métodos auxiliares secundários que você não deseja configurar repetidamente em cada teste e você preferir que o teste foque apenas nas asserções de saída do Sistema Sob Teste.
Resumo
Nesta aula, aprendemos quatro pontos essenciais:
- Um Strict Stub garante que o nosso caso de teste falhe imediatamente se o Sistema Sob Teste invocar um método cujo comportamento não foi explicitamente configurado.
- No Mockito clássico, criávamos um strict stub substituindo o
Answerpadrão por um que lança exceção, o que nos forçava a usar a sintaxe invertidawillReturn(...).given(...)/doReturn(...).when(...). - No MockK, o comportamento padrão de
mockk<T>()(relaxed = false) já é estrito nativamente e preserva a sintaxe naturalevery { ... } returns .... - Podemos escolher entre
strictStub()(relaxed = false) estub()(relaxed = true) na nossa classeTestDoublesde acordo com o nível de rigor desejado para o teste.