Introdução ao MockK para Desenvolvedores Kotlin

Ao concluirmos esta aula, nós:

  • Seremos capazes de declarar a dependência necessária do MockK com Gradle (Kotlin DSL) e Maven.
  • Saberemos o que é o MockK (e como ele evoluiu a partir do clássico Mockito para o mundo Kotlin).
  • Compreenderemos as três regras fundamentais que devemos levar em consideração ao criar dublês de teste (test doubles) com o MockK.

Obtendo as Dependências Necessárias

Antes de podermos criar dublês de teste com o MockK (o equivalente nativo em Kotlin ao mockito-core), precisamos declarar a dependência io.mockk:mockk em nosso script de build.

Se estivermos utilizando Maven, declaramos essa dependência adicionando o seguinte trecho ao nosso arquivo pom.xml:

1
2
3
4
5
6
<dependency>
    <groupId>io.mockk</groupId>
    <artifactId>mockk-jvm</artifactId>
    <version>1.14.2</version>
    <scope>test</scope>
</dependency>

Se estivermos utilizando Gradle 9 (Kotlin DSL), declaramos essa dependência adicionando o seguinte trecho ao nosso arquivo build.gradle.kts:

build.gradle.kts linhas 21–34
Arquivo Real KOTLIN
21
22
23
24
25
26
27
28
29
30
31
32
33
34
dependencies {
    implementation("org.springframework.boot:spring-boot-starter:4.1.2")
    implementation("org.springframework.boot:spring-boot-starter-web:4.1.2")
    implementation("org.springframework.boot:spring-boot-starter-validation:4.1.2")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib")

    testImplementation("org.springframework.boot:spring-boot-starter-test:4.1.2") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
    testImplementation("org.junit.jupiter:junit-jupiter:6.0.3")
    testImplementation("org.junit.platform:junit-platform-suite:1.11.0")
    testImplementation("org.assertj:assertj-core:3.27.7")

[!NOTE] Nota sobre compatibilidade com Mockito Caso você trabalhe em um projeto legado misto Java/Kotlin que ainda utilize Mockito, a dependência correspondente é org.mockito:mockito-core (ou org.mockito.kotlin:mockito-kotlin). Neste curso, utilizamos o MockK como padrão principal por ser 100% escrito para Kotlin (suportando nativamente classes final, data classes, funções suspend, extension functions e object), mas sempre mostraremos o paralelo conceitual com o Mockito para que você domine ambos!

Depois de adicionarmos a dependência io.mockk:mockk ao nosso classpath de testes, já podemos criar dublês de teste. Vamos conhecer os fundamentos do MockK!


MockK 101 (e o Paralelo com o Mockito)

O MockK é o framework de mocking padrão da comunidade Kotlin, utilizado para criar diferentes tipos de dublês de teste: stubs, mocks, spies e dummies.

Introdução às APIs do MockK (e como elas resolvem o dilema do BDDMockito)

No Java clássico com Mockito, existiam duas APIs separadas:

  • A classe org.mockito.Mockito fornecia a API original baseada na sintaxe when(...).thenReturn(...).
  • A classe org.mockito.BDDMockito fornecia a API voltada ao estilo BDD (given(...).willReturn(...) e then(...).should()). Ela foi criada no Mockito porque a palavra when da API original entrava em conflito semântico com os comentários // given, // when, // then (já que a configuração do stub fica na seção // given, e não na seção // when!). además disso, em Kotlin, when é uma palavra reservada da linguagem, o que obrigava o uso de crases feias no Mockito (`when`(...)).

O MockK resolveu ambos os problemas de forma brilhante ao projetar uma DSL única, limpa e natural para o estilo BDD (Given-When-Then) em Kotlin:

  • every { ... } returns ... (ou coEvery para corrotinas): Usado na seção // Given para configurar o comportamento de um stub ou mock (substituindo tanto Mockito.when() quanto BDDMockito.given()).
  • verify { ... } (ou coVerify para corrotinas): Usado na seção // Then para verificar as interações ocorridas entre o Sistema Sob Teste e o nosso mock (substituindo Mockito.verify() e BDDMockito.then().should()).

Vamos avançar e descobrir o que devemos levar em consideração quando criamos dublês de teste com o MockK.


Criando Dublês de Teste com o MockK: As 3 Regras de Ouro

Antes de criarmos dublês de teste com o MockK, devemos conhecer estas três regras fundamentais:

Primeira Regra: Dublês de Teste NÃO são Stateless

Os dublês de teste criados com o MockK (assim como no Mockito) não são stateless — eles guardam tanto a configuração de respostas quanto o histórico de chamadas recebidas.

Isso significa que, ao escrevermos testes unitários, devemos criar um novo dublê de teste antes que cada método de teste seja invocado. Isso garante o isolamento total entre os testes e impede que um método de teste falhe (ou passe por engano) por reutilizar um dublê de teste “sujo” por um teste anterior.

[!NOTE] E quanto ao clearMocks() / Mockito.reset()? Tanto o MockK (clearMocks(mock)) quanto o Mockito (Mockito.reset(mock)) oferecem funções para “resetar” um dublê de teste existente. Porém, se estivermos escrevendo testes unitários puros, não devemos resetar nossos dublês de teste, pois criar uma instância nova em folha no @BeforeEach é mais simples, rápido e evita complexidade desnecessária na classe de teste. Resetar dublês só é útil em testes de integração onde o dublê de teste é gerenciado por um container IoC (como o contexto do Spring).

Segunda Regra: Como e Onde Instanciar Nossos Dublês de Teste

Devemos criar nossos dublês de teste utilizando uma destas duas funções principais do pacote io.mockk:

  • Se quisermos criar um stub, um mock ou um dummy, devemos utilizar a função mockk<T>() (o equivalente a Mockito.mock(T::class.java)).
  • Se quisermos criar um spy que envolve um objeto real, devemos invocar a função spyk(realObject) (o equivalente a Mockito.spy(realObject)).

Além disso, defendemos fortemente que você deve instanciar seus dublês de teste e o Sistema Sob Teste explicitamente dentro de um método de configuração anotado com @BeforeEach (em vez de usar anotações mágicas como @MockK + @InjectMockKs + @ExtendWith(MockKExtension::class)). Essa abordagem explícita traz dois benefícios importantes:

  1. Percebemos imediatamente se o Sistema Sob Teste possui dependências demais: Instanciar dublês de teste manualmente em um método @BeforeEach torna visível a dor de uma classe acoplada a 8 ou 10 dependências. Se configurar o @BeforeEach ficar irritante demais, esse é o alerta perfeito de design indicando que nossa classe tem responsabilidades demais (violando o Single Responsibility Principle) e precisa ser refatorada!
  2. Não acoplamos nosso teste a extensões ou runners de reflexão: Usar @MockK e @InjectMockKs esconde o problema do excesso de dependências debaixo do tapete. Por isso, em todo este curso, criaremos nossos dublês de forma explícita e transparente com mockk<T>().

[!TIP] Exceção para Dummies locais Se precisarmos criar um dummy que é usado por apenas um método de teste específico, criá-lo diretamente dentro do próprio método de teste (@Test) é uma excelente opção, pois mantém o método @BeforeEach limpo e torna o teste ainda mais fácil de ler.

Terceira Regra: Não Coloque o Tipo do Dublê no Nome do Campo ou Variável

Não devemos adicionar o tipo técnico do dublê de teste (Stub, Mock, Spy) ao nome do campo ou variável que guarda a referência para o dublê criado (evite nomes como taskFinderStub ou taskRepositoryMock).

Isso é uma má ideia porque o tipo técnico do dublê não descreve o propósito de domínio daquele colaborador perante o Sistema Sob Teste. Devemos lembrar que um dublê de teste não é apenas um stub, mock, spy ou dummy: ele representa um colaborador exigido pelo Sistema Sob Teste, e seu nome deve refletir seu papel no domínio!

Veja três exemplos práticos que demonstram esse ponto:

  • Se criarmos um stub de TaskFinder que busca tarefas por um critério específico, devemos nomear o campo como taskFinder ou finder.
  • Se criarmos um mock de TaskRepository que fornece operações CRUD para objetos Task, devemos nomear o campo como taskRepository ou repository, pois esse objeto atua como o elo entre nossa aplicação e o banco de dados.
  • Se criarmos um dummy que é irrelevante para o nosso caso de teste, devemos nomear a variável como notImportant ou notUsed, deixando explícito para o leitor que aquele argumento é irrelevante para o cenário testado.

Validando a Configuração do MockK com JUnit 6

Para certificar que nossa dependência do MockK está operacional e aderente às regras de ouro de ciclo de vida e isolamento dos dublês em tempo de execução, adicionamos a seguinte classe de verificação de sanidade:

MockKConfigTest.kt
Arquivo Real KOTLIN
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.testwithspring.starter.mockk

import io.mockk.mockk
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test

@DisplayName("01 - Configuração e Fundamentos do MockK")
class MockKConfigTest {

    private lateinit var repository: TaskRepository

    @BeforeEach
    fun configureDoubles() {
        repository = mockk(relaxed = true)
    }

    @Test
    fun `deve instanciar duble com mockk sem depender de reflexao ou runners magicos`() {
        assertThat(repository).isNotNull()
    }

    @Test
    fun `duble deve ser isolado a cada execucao de teste`() {
        assertThat(repository.count()).isZero()
    }
}

Executando Nossos Testes no Gradle

Ao executarmos a suíte de verificação de configuração do MockK através do Gradle (./gradlew test --tests MockKConfigTest -i), inspecionamos a evidência real de compilação e execução dos testes:

Evidência Real de Execução: ./gradlew test --tests MockKConfigTest -i
Terminal Log
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
Compiling with Kotlin compiler version 2.4.21 (JVM target 21)
Compiling Kotlin files from [/home/wc/test-with-spring/code/01-starter-junit6/src/test/kotlin]

> Task :processTestResources NO-SOURCE
> Task :testClasses
> 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.MockKConfigTest > deve instanciar duble com mockk sem depender de reflexao ou runners magicos PASSED
com.testwithspring.starter.mockk.MockKConfigTest > duble deve ser isolado a cada execucao de teste PASSED

01 - Configuração e Fundamentos do MockK > 2 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.007 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.014 secs) into: /home/wc/test-with-spring/code/01-starter-junit6/build/reports/tests/test

BUILD SUCCESSFUL in 1s
5 actionable tasks: 2 executed, 3 up-to-date

Resumo

Nesta aula, aprendemos cinco pontos fundamentais:

  • Antes de podermos criar dublês de teste com o MockK, devemos declarar a dependência io.mockk:mockk no escopo de teste do nosso script de build (build.gradle.kts ou pom.xml).
  • Podemos criar stubs, mocks, spies e dummies utilizando o MockK (mockk<T>() e spyk()).
  • Devemos criar uma nova instância dos nossos dublês de teste antes que cada método de teste seja invocado, pois dublês de teste possuem estado.
  • Devemos instanciar nossos dublês de teste explicitamente em um método de setup anotado com @BeforeEach (exceto dummies pontuais, que podem ser criados no próprio método @Test).
  • Não devemos adicionar o sufixo técnico (Mock, Stub, Spy) ao nome do campo ou variável que referencia o dublê de teste; devemos usar nomes que reflitam seu papel no domínio (ou notImportant / notUsed no caso de um dummy).