Criando Dummies e Relaxed Mocks com MockK
Ao concluirmos esta aula, nós:
- Saberemos como criar objetos Dummy utilizando o MockK (
mockk<T>(relaxed = true)). - Compreenderemos o que acontece quando o Sistema Sob Teste invoca um método de um Dummy (por exemplo, em uma instrução de log) e por que lançar exceções em Dummies torna os testes frágeis.
- Saberemos como destacar de forma cristalina no código que o dublê de teste criado é um Dummy criando a função auxiliar
dummy<T>().
[!NOTE] Esta aula assume que:
Introdução ao Sistema Sob Teste (SUT)
O nosso Sistema Sob Teste (TaskService) possui um método chamado createCopyOf(), que cria uma cópia de uma tarefa existente e retorna uma referência para o novo objeto Task criado. Esse método recebe dois parâmetros:
- O ID da tarefa de origem (
sourceTaskId: Long). - Um objeto
LoggedInUser(creator: LoggedInUser) que contém as informações do usuário autenticado que será registrado como criador da nova tarefa.
Como esta aula se concentra na criação de dummies com o MockK, a implementação completa do método createCopyOf() não é relevante agora. A parte relevante da classe TaskService é a seguinte:
|
|
A classe LoggedInUser contém as informações de um usuário autenticado, e seu código-fonte é o seguinte:
|
|
Vamos descobrir como podemos criar dummies com o MockK.
Criando Dummies com o MockK
A primeira coisa que precisamos compreender é que nem o MockK nem o Mockito diferenciam estruturalmente dummies, stubs e mocks na hora da criação:
- No Mockito, existe apenas o método
Mockito.mock(T::class.java)para criar dummies, stubs e mocks. - No MockK, existe a função
mockk<T>()para criar dummies, stubs e mocks, e qualquer dublê criado com essa função tem potencial técnico para atuar como um mock.
Por essa razão, devemos ter cuidado redobrado e garantir que o propósito do nosso dublê de teste fique cristalino para qualquer pessoa que leia o nosso código de teste.
Como vimos anteriormente, a primeira forma de destacar que um dublê de teste é um dummy é dar um nome descritivo à variável que o armazena (como notImportant). Por exemplo, um método de teste que cria um dummy de LoggedInUser fica assim:
|
|
[!NOTE] Tecnicamente falando, a constante
SOURCE_TASK_IDtambém atua como um dummy neste exemplo específico, pois seu valor não altera o resultado aqui. Contudo, usamos um valor realista (99L) porque ele será utilizado nas próximas aulas deste tópico.
Agora surge uma pergunta fundamental:
O que acontece se o Sistema Sob Teste invocar um método (ou acessar uma propriedade) do nosso objeto Dummy?
Aqui existe uma diferença importante entre o comportamento padrão do Mockito e o comportamento padrão do MockK, e compreendê-la é essencial!
No Mockito, quando criamos um dublê sem configurar uma resposta padrão (mock(LoggedInUser.class)), ele é “relaxado” por padrão: se alguém chamar creator.getUsername(), o Mockito retorna um valor “vazio” (null, 0, false ou uma coleção vazia).
Já no MockK, um mockk<LoggedInUser>() simples é estrito por padrão (strict): se qualquer método ou propriedade dele for invocado sem ter sido configurado com every { ... }, o MockK lança imediatamente um MockKException: no answer found for: LoggedInUser.getUsername()!
Como um dummy é irrelevante para o nosso caso de teste, poderíamos pensar a princípio: “Não seria uma boa ideia deixar o dummy estrito (ou configurar um Answer no Mockito para lançar RuntimeException) para garantir que o teste falhe caso o Sistema Sob Teste toque no nosso dummy?”
Na verdade, essa é uma péssima ideia! Vamos entender exatamente o porquê.
Suponha que no Mockito configurássemos um Answer que lança RuntimeException("The system under test invoked a dummy.") — ou que no MockK criássemos o dummy como mockk<LoggedInUser>() estrito (sem relaxed = true):
|
|
Quando executamos esse teste, descobrimos que ele falha com uma exceção! Isso significa que o objeto LoggedInUser deixou de ser um dummy para o nosso cenário de teste?
Não! O nosso caso de teste falhou única e exclusivamente porque o Sistema Sob Teste escreve uma mensagem de log diagnóstica no início do método:
|
|
Nós não estamos interessados nessa linha de log durante o teste de negócio, e definitivamente não queremos que uma simples linha de LOGGER.info que acessa creator.username quebre nossos testes ou nos obrigue a fazer stubbing manual de um objeto irrelevante!
É exatamente por isso que:
- No Mockito, jamais devemos configurar um
Answerque lance exceção em um dummy, pois o retorno padrão “vazio” do Mockito (null/""/0) permite que instruções de log passem sem quebrar o teste. - No MockK, ao criarmos um Dummy, devemos utilizar
mockk<T>(relaxed = true)! Comrelaxed = true, o MockK retorna valores padrão seguros e silenciosos (""paraString,0LparaLong,falseparaBoolean, listas vazias etc.) caso o Sistema Sob Teste acesse alguma propriedade do dummy em uma linha de log, evitando que nossos testes fiquem frágeis (brittle).
[!NOTE] E se passarmos o objeto inteiro para o Logger (
LOGGER.info(..., creator))? Mesmo que o logger receba o objeto inteirocreatorem vez decreator.username, o SLF4J/Logback invocarácreator.toString(). Portanto, manter o dummy relaxado (relaxed = true) garante que nemtoString()nem leituras de propriedades de log comocreator.userIdoucreator.usernamequebrem o teste indevidamente.
Já sabemos como criar dummies seguros com o MockK. Contudo, podemos tornar esse processo ainda mais expressivo e legível! Vamos descobrir como.
Indo Além: Criando o Object Mother TestDoubles.dummy()
Como dissemos anteriormente, tanto o MockK (mockk()) quanto o Mockito (mock()) usam a palavra “mock” no nome da função que cria dublês de teste. O problema é que ler mockk<LoggedInUser>(relaxed = true) no meio do teste ainda contém a palavra mockk, o que pode sugerir visualmente que o objeto criado é um mock (e não um dummy).
Isso pode parecer um detalhe pequeno, mas se quisermos destacar com clareza cirúrgica o papel de cada dublê de teste em nossa suíte, podemos resolver esse problema facilmente seguindo estes três passos:
Passo 1: Criar o objeto utilitário TestDoubles
Primeiro, criamos um object TestDoubles (ou arquivo de funções utilitárias de teste). Em Kotlin, um object já é final e possui construtor privado automaticamente, impedindo instanciação ou herança indevida:
|
|
Passo 2: Adicionar o método fábrica dummy()
Segundo, adicionamos uma função fábrica genérica (usando inline e reified T : Any do Kotlin, além da sobrecarga com KClass / Class se desejado) chamada dummy() dentro de TestDoubles. Essa função delega internamente para mockk<T>(relaxed = true) e retorna o dublê de teste criado:
|
|
Passo 3: Utilizar dummy() em nosso método de teste
Terceiro, modificamos nosso método de teste para criar o objeto dummy utilizando nossa nova função dummy():
|
|
Olhe para a linha val notImportant: LoggedInUser = dummy() (ou val notImportant = dummy<LoggedInUser>()):
Agora utilizamos dois mecanismos complementares para destacar o fato de que o dublê de teste criado é um dummy:
- O nome da variável (
notImportantounotUsed) que referencia o nosso dummy. - A criação do objeto invocando um método que se chama explicitamente
dummy()(em vez demockk()).
Fazer esse pequeno esforço extra vale muito a pena porque, na prática, muitas equipes chamam qualquer dublê de teste indiscriminadamente de “mock”. Quando usamos val notImportant = dummy<LoggedInUser>(), eliminamos qualquer ambiguidade e comunicamos nossa intenção imediatamente para quem lê o teste!
Executando Nossos Testes no Gradle
Ao executarmos a suíte TaskServiceTest através do Gradle (./gradlew test --tests TaskServiceTest -i), verificamos a execução dos testes com dublês relaxados e funções utilitárias:
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.TaskServiceTest > deve criar dummy dentro do metodo de teste PASSED
com.testwithspring.starter.mockk.TaskServiceTest > deve criar dummy usando o metodo utilitario dummy PASSED
02 - Criando Dummies e Relaxed Mocks com 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 três lições práticas:
- As bibliotecas de mocking (MockK e Mockito) usam a mesma função base (
mockk()/mock()) para criar dummies, stubs e mocks. - Um dummy não deve lançar exceção caso uma instrução inocente de log acesse uma de suas propriedades; por isso, no MockK, criamos dummies com
relaxed = true(e no Mockito mantemos a resposta padrão sem lançar exceções). - Não devemos invocar
mockk()diretamente na classe de teste quando quisermos criar um dummy: encapsular a criação em uma função semânticadummy<T>()combinada com um nome de variável comonotImportanttorna a intenção do teste 100% explícita.