Dominando o MockK, Parte 1: Mocks de Classes e Objetos

Ao concluir esta aula, seremos capazes de:

  • Entender o que é o MockK e seus principais recursos nativos para Kotlin.
  • Obter as dependências necessárias com Gradle (Kotlin DSL) e Maven.
  • Criar dublês de teste (test doubles) com o MockK manualmente e com anotações.
  • Compreender como integrar o MockK com o JUnit Jupiter 6.

Esta aula assume que:

O Que É o MockK?

O MockK é uma biblioteca de mocking projetada especificamente para Kotlin. Suas principais características são:

  • Suporta mocks, stubs e spies (espiões), além de classes final e object por padrão.
  • Suporta tanto stubs/mocks estritos (strict) quanto relaxados (relaxed).
  • Possui um suporte extremamente versátil para correspondência de argumentos (argument matching).
  • Permite criar dublês de teste utilizando tanto a abordagem manual quanto anotações.
  • Fornece suporte integrado para JUnit Jupiter (JUnit 5/6) e ecossistema Spring Boot.

A seguir, veremos como obter as dependências necessárias com Maven e Gradle.

Obtendo as Dependências Necessárias

Ao utilizar Kotlin 2.4.21 e Coroutines modernas no ecossistema do Spring Boot 4.1.2, declaramos a dependência mockk na versão 1.14.0 (família MockK 1.14) em nosso script de build.

Se estivermos utilizando o Maven, podemos obter essa dependência adicionando a seguinte declaração à seção dependencies do nosso arquivo pom.xml:

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

Se estivermos utilizando o Gradle 9.8.2 (com Kotlin DSL), devemos adicionar a dependência mockk à configuração testImplementation. Podemos fazer isso adicionando a seguinte linha ao bloco dependencies do nosso arquivo build.gradle.kts:

1
2
3
dependencies {
    testImplementation("io.mockk:mockk:1.14.0")
}

Nota Histórica e Compatibilidade:

Nas versões iniciais do MockK (à época do Kotlin 1.2 e 1.3), existiam artefatos específicos por versão da linguagem (como 1.9.3.kotlin12). No MockK 1.14 com Kotlin 2.4.21 e JDK 21+, o artefato padrão io.mockk:mockk:1.14.0 resolve automaticamente a variante JVM compatível com o compilador K2 e com o JUnit Platform 6.

Vamos avançar e descobrir como podemos criar nossos dublês de teste utilizando a abordagem manual.

Criando Dublês de Teste com a Abordagem Manual

Antes de continuarmos, vale destacar que o MockK não diferencia estruturalmente stubs e mocks no momento da instanciação. Isso significa que, se quisermos criar tanto um stub quanto um mock, utilizamos a mesma função chamada mockk().

Embora o nome dessa função sugira que o objeto criado seja sempre um mock, utilizamos a função mockk() para ambos os papéis porque isso mantém a API simples e concisa.

A seguir, criaremos stubs, mocks e spies com o MockK. Observe que todos os exemplos a seguir substituem uma lista (List / MutableList) ou um objeto pelo dublê de teste criado.

Criando Stubs

Se quisermos criar um stub estrito (strict stub), devemos seguir estes passos:

  1. Adicionar uma propriedade lateinit à nossa classe de teste e garantir que essa propriedade armazene uma lista de objetos Long (List<Long>).
  2. Adicionar uma função de configuração (setup) à nossa classe de teste e garantir que ela seja invocada antes da execução de cada função de teste (@BeforeEach). Criaremos o nosso stub dentro desta função.
  3. Criar o nosso stub invocando a função mockk() e armazenar o stub criado na propriedade list. Ao invocar a função mockk(), devemos configurar o tipo do stub criado via parâmetro de tipo genérico.

Após criarmos um stub estrito, o código-fonte da nossa classe de teste ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag

@Tag("unitTest")
@DisplayName("Demonstra como podemos criar um stub estrito")
class StrictStubExampleTest {

    private lateinit var list: List<Long>

    @BeforeEach
    fun createStub() {
        list = mockk<List<Long>>()
    }
}

Se quisermos criar um stub relaxado (relaxed stub), devemos seguir estes passos:

  1. Adicionar uma propriedade lateinit à nossa classe de teste e garantir que essa propriedade contenha uma lista de objetos (List<*>). Utilizamos a projeção estrela (star projection) aqui porque o stubbing relaxado automático precisa evitar armadilhas de apagamento de tipo (type erasure) ou ClassCastException ao sintetizar valores de retorno genéricos não configurados.
  2. Adicionar uma função de configuração (setup) à nossa classe de teste e garantir que essa função seja invocada antes da execução de cada teste. Criaremos o nosso stub nesta função.
  3. Criar o nosso stub invocando a função mockk() e armazenar o stub criado na propriedade list. Ao invocar a função mockk(), configuramos o tipo do stub criado e definimos o valor do parâmetro nomeado relaxed como true.

Após criarmos um stub relaxado, o código-fonte da nossa classe de teste ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag

@Tag("unitTest")
@DisplayName("Demonstra como podemos criar um stub relaxado")
class RelaxedStubExampleTest {

    private lateinit var list: List<*>

    @BeforeEach
    fun createStub() {
        list = mockk<List<*>>(relaxed = true)
    }
}

Leitura Adicional:

Vamos avançar e descobrir como podemos criar mocks com o MockK.

Criando Mocks

Se quisermos criar um mock estrito (strict mock), devemos seguir estes passos:

  1. Adicionar uma propriedade lateinit à nossa classe de teste e garantir que essa propriedade contenha uma lista de objetos Long (List<Long>).
  2. Adicionar uma função de configuração (setup) à nossa classe de teste e garantir que ela seja invocada antes da execução de cada função de teste. Criaremos o nosso mock dentro desta função.
  3. Criar o nosso mock invocando a função mockk() e armazenar o mock criado na propriedade list. Ao invocar a função mockk(), devemos configurar o tipo do mock criado.

Após criarmos um mock estrito, o código-fonte da nossa classe de teste ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag

@Tag("unitTest")
@DisplayName("Demonstra como podemos criar um mock estrito")
class StrictMockExampleTest {

    private lateinit var list: List<Long>

    @BeforeEach
    fun createMock() {
        list = mockk<List<Long>>()
    }
}

Se quisermos criar um mock relaxado (relaxed mock), devemos seguir estes passos:

  1. Adicionar uma propriedade lateinit à nossa classe de teste e garantir que essa propriedade contenha uma lista de objetos Long (List<Long>). Observe que não precisamos obrigatoriamente usar a projeção estrela (*) aqui, desde que não dependamos do retorno automático de funções genéricas não configuradas.
  2. Adicionar uma função de configuração (setup) à nossa classe de teste e garantir que ela seja invocada antes da execução de cada função de teste. Criaremos o nosso mock nesta função.
  3. Criar o nosso mock invocando a função mockk() e armazenar o mock criado na propriedade list. Ao invocar a função mockk(), devemos configurar o tipo do mock criado e definir o valor do parâmetro relaxed como true.

Após criarmos um mock relaxado, o código-fonte da nossa classe de teste ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import io.mockk.mockk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag

@Tag("unitTest")
@DisplayName("Demonstra como podemos criar um mock relaxado")
class RelaxedMockExampleTest {

    private lateinit var list: List<Long>

    @BeforeEach
    fun createMock() {
        list = mockk<List<Long>>(relaxed = true)
    }
}

Se quisermos fazer mock ou stub de objetos singleton do Kotlin (object), devemos seguir estes passos:

  1. Adicionar uma função de configuração (setup) à nossa classe de teste e garantir que essa função seja invocada antes da execução de cada função de teste (@BeforeEach). Criaremos o nosso mock ou stub nesta função.
  2. Substituir o object Kotlin pelo nosso dublê de teste. Podemos fazer isso invocando a função mockkObject(). Ao invocar essa função, devemos especificar o objeto Kotlin que será transformado em dublê de teste.
  3. Adicionar uma função de limpeza (teardown) à nossa classe de teste e garantir que essa função seja invocada após a execução de cada função de teste (@AfterEach). Destruiremos o nosso dublê de teste nesta função para restaurar o estado original do singleton.
  4. Destruir o nosso dublê de teste invocando a função unmockkObject(). Ao invocar essa função, devemos especificar o objeto Kotlin que havia sido substituído pelo dublê de teste.

Após substituirmos um objeto Calculator por um mock, o código-fonte da nossa classe de teste ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import io.mockk.mockkObject
import io.mockk.unmockkObject
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag

@Tag("unitTest")
@DisplayName("Demonstra como podemos fazer mock de objetos Kotlin")
class ObjectMockExampleTest {

    @BeforeEach
    fun createMock() {
        mockkObject(Calculator)
    }

    @AfterEach
    fun destroyMock() {
        unmockkObject(Calculator)
    }
}

Há dois pontos importantes que quero destacar:

  • Podemos usar exatamente esta mesma técnica se quisermos substituir um object Kotlin por um stub.
  • Se tivermos substituído múltiplos objetos Kotlin por dublês de teste, também podemos destruir todos os nossos dublês de uma só vez invocando a função unmockkAll().

Leitura Adicional:

A seguir, aprenderemos a criar spies (espiões) com o MockK.

Criando Spies

Se quisermos criar um spy, devemos seguir estes passos:

  1. Adicionar uma propriedade lateinit à nossa classe de teste e garantir que essa propriedade contenha uma lista mutável de objetos Long (MutableList<Long>).
  2. Adicionar uma função de configuração (setup) à nossa classe de teste e garantir que ela seja invocada antes da execução de cada função de teste. Criaremos o nosso spy nesta função.
  3. Criar o nosso spy invocando a função spyk() e armazenar o spy criado na propriedade list. Ao invocar a função spyk(), devemos configurar o tipo concreto do spy criado (por exemplo, ArrayList<Long>).

Após criarmos um spy, o código-fonte da nossa classe de teste ficará da seguinte forma:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import io.mockk.spyk
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag

@Tag("unitTest")
@DisplayName("Demonstra como podemos criar um spy")
class SpyExampleTest {

    private lateinit var list: MutableList<Long>

    @BeforeEach
    fun createSpy() {
        list = spyk<ArrayList<Long>>()
    }
}

Leitura Adicional:

Agora já sabemos criar diferentes dublês de teste usando a abordagem manual. Vamos avançar e descobrir como podemos criar nossos dublês de teste utilizando as anotações do MockK.

Criando Dublês de Teste com Anotações

Quando queremos criar dublês de teste utilizando as anotações do MockK, podemos recorrer às seguintes anotações:

  • A anotação @MockK nos permite criar tanto stubs quanto mocks estritos e relaxados. Se quisermos criar um stub ou mock relaxado com ela, devemos definir o valor do atributo relaxed da anotação @MockK como true.
  • A anotação @RelaxedMockK nos permite criar stubs e mocks relaxados diretamente.
  • A anotação @SpyK nos permite criar spies envolvendo uma instância existente.

A seguir, criaremos diferentes dublês de teste utilizando essas anotações. Podemos criar nossos dublês de teste seguindo estes passos:

  1. Adicionar uma propriedade lateinit à nossa classe de teste anotada com @MockK e garantir que ela contenha um mock estrito.
  2. Adicionar uma propriedade lateinit à nossa classe de teste anotada com @MockK(relaxed = true) e garantir que ela contenha um mock relaxado.
  3. Adicionar uma nova propriedade inicializada à nossa classe de teste anotada com @SpyK e garantir que ela contenha um spy.
  4. Adicionar uma propriedade lateinit à nossa classe de teste anotada com @MockK e garantir que ela contenha um stub estrito.
  5. Adicionar uma propriedade lateinit à nossa classe de teste anotada com @RelaxedMockK e garantir que ela contenha um stub relaxado.
  6. Adicionar uma função de configuração (setup) à nossa classe de teste e garantir que essa função seja invocada antes da execução de cada função de teste. Inicializaremos nossos dublês de teste nesta função.
  7. Criar nossos dublês de teste invocando a função MockKAnnotations.init(this). Ao invocar essa função, devemos passar a instância de teste (this) que contém os campos ou propriedades onde nossos dublês de teste serão injetados.

Após escrevermos uma classe de teste que cria nossos dublês de teste com as anotações do MockK, seu código-fonte ficará da seguinte forma:

 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
29
30
import io.mockk.MockKAnnotations
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.impl.annotations.SpyK
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName

@DisplayName("Demonstra como podemos criar dublês de teste com anotações")
class AnnotationTest {

    @MockK
    private lateinit var mockList: List<Long>

    @MockK(relaxed = true)
    private lateinit var relaxedMockList: List<Long>

    @SpyK
    private var spyList = ArrayList<Long>()

    @MockK
    private lateinit var stubList: List<Long>

    @RelaxedMockK
    private lateinit var relaxedStubList: List<*>

    @BeforeEach
    fun createTestDoubles() {
        MockKAnnotations.init(this)
    }
}

Antes de continuarmos, quero ressaltar que não sou um grande fã do uso de anotações para essa finalidade, pois elas tornam nossa vida “fácil demais”. Os desenvolvedores de diferentes bibliotecas de mocking adicionaram suporte a anotações porque os usuários reclamavam que criar dublês de teste manualmente exigia muito código.

O problema é que as anotações ajudam a esconder problemas de design presentes no nosso código. Acredito que, se configurar o sistema sob teste (System Under Test - SUT) for trabalhoso demais manualmente, é um sinal claro de que o sistema sob teste possui dependências em excesso e precisa ser refatorado. É por isso que recomendo preferir a criação explícita de dublês de teste em vez de anotações.

Leitura Adicional:

A seguir, veremos como podemos criar nossos dublês de teste usando anotações integradas diretamente ao modelo de extensões do JUnit Jupiter (JUnit 5 / JUnit 6).

Integrando o MockK com o JUnit Jupiter (JUnit 5 / 6)

Se estivermos utilizando o JUnit Jupiter 6.0.3 (cuja API de extensão mantém o pacote org.junit.jupiter.api.extension introduzido no JUnit 5) e quisermos criar nossos dublês de teste usando as anotações do MockK, podemos delegar a inicialização para a extensão MockKExtension. Para usar essa extensão, devemos fazer as seguintes alterações na classe de teste que criamos na seção anterior:

  1. Remover a função de configuração (@BeforeEach) da nossa classe de teste.
  2. Garantir que a MockKExtension seja invocada quando executarmos os testes da nossa classe anotando-a com @ExtendWith(MockKExtension::class).

Após fazermos essas alterações em nossa classe de teste, seu código-fonte ficará da seguinte forma:

 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
import io.mockk.impl.annotations.MockK
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.impl.annotations.SpyK
import io.mockk.junit5.MockKExtension
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(MockKExtension::class)
@DisplayName("Demonstra como podemos integrar o MockK com o JUnit Jupiter")
class JUnit5Test {

    @MockK
    private lateinit var mockList: List<Long>

    @MockK(relaxed = true)
    private lateinit var relaxedMockList: List<Long>

    @SpyK
    private var spyList = ArrayList<Long>()

    @MockK
    private lateinit var stubList: List<Long>

    @RelaxedMockK
    private lateinit var relaxedStubList: List<*>
}

Leitura Adicional:

Agora sabemos como criar nossos dublês de teste com o MockK e como integrá-lo ao JUnit Jupiter. Vamos resumir o que aprendemos nesta aula.

Resumo

Esta aula nos ensinou quatro conceitos fundamentais:

  • Podemos criar mocks e stubs utilizando a função mockk(), a anotação @MockK ou a anotação @RelaxedMockK.
  • Se quisermos fazer mock de objetos singleton do Kotlin (object), devemos utilizar as funções mockkObject() e unmockkObject() (ou unmockkAll()).
  • Podemos criar spies utilizando a função spyk() ou a anotação @SpyK.
  • Se quisermos criar nossos dublês de teste com anotações, devemos inicializá-los utilizando a função MockKAnnotations.init(this) ou registrando a extensão MockKExtension no JUnit Jupiter.