Criando Stubs com every { ... } returns

Ao concluirmos esta aula, nós:

  • Compreenderemos como destacar explicitamente no código que o dublê de teste criado é um Stub (TestDoubles.stub()).
  • Seremos capazes de configurar um stub para retornar um objeto ou lançar uma exceção quando seu método for invocado (every { ... } returns ... e throws).
  • Saberemos como configurar os parâmetros esperados de um método utilizando Argument Matchers (eq(), any(), ofType()).
  • Saberemos como acessar os argumentos reais passados para o método stubado usando blocos answers { ... }.

[!NOTE] Esta aula assume que:


Destacando o Tipo de um Stub (TestDoubles.stub())

Como vimos na aula anterior, nem o MockK nem o Mockito diferenciam dummies, stubs e mocks na hora da instanciação: a função mockk() (ou Mockito.mock()) serve para criar todos os três, e qualquer dublê criado com ela pode atuar como um mock.

Na aula anterior, criamos o objeto utilitário TestDoubles com um método fábrica chamado dummy(). Aprendemos que explicitar o tipo do dublê de teste no método fábrica esclarece nossas intenções e reduz mal-entendidos na equipe.

Por esse motivo, adicionaremos agora um método fábrica chamado stub() ao nosso objeto TestDoubles. Esse método delega a criação para o MockK e retorna o dublê de teste criado. Após adicionarmos esse método a TestDoubles, seu código-fonte fica assim:

TestDoubles.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
29
30
31
32
33
34
package com.testwithspring.starter.mockk

import io.mockk.mockk
import io.mockk.mockkClass

object TestDoubles {

    inline fun <reified T : Any> dummy(): T {
        return mockk<T>(relaxed = true)
    }

    @JvmStatic
    fun <T : Any> dummy(dummyClass: Class<T>): T {
        return mockkClass(dummyClass.kotlin, relaxed = true)
    }

    inline fun <reified T : Any> strictStub(): T {
        return mockk<T>(relaxed = false)
    }

    @JvmStatic
    fun <T : Any> strictStub(stubClass: Class<T>): T {
        return mockkClass(stubClass.kotlin, relaxed = false)
    }

    inline fun <reified T : Any> stub(): T {
        return mockk<T>(relaxed = true)
    }

    @JvmStatic
    fun <T : Any> stub(stubClass: Class<T>): T {
        return mockkClass(stubClass.kotlin, relaxed = true)
    }
}

[!NOTE] Por que stub() usa relaxed = true aqui? No Mockito clássico, um stub criado com Mockito.mock(stubClass) retorna valores vazios (null, 0, false) caso algum método não configurado seja chamado. Na próxima aula, compararemos em detalhes os Stubs Relaxados com os Stubs Estritos (strictStub(), que equivale ao mockk<T>() padrão sem relaxed = true)!

Agora podemos criar stubs sem invocar mockk() diretamente em nossas classes de teste. Antes de colocarmos a mão na massa, vamos dar uma rápida olhada na interface que será substituída pelo nosso stub.


Introdução ao Sistema Sob Teste (SUT)

O nosso Sistema Sob Teste possui uma única dependência chamada TaskRepository. Essa interface declara quatro métodos descritos a seguir:

  • O método count() retorna o número total de tarefas encontradas no banco de dados (Long).
  • O método delete() exclui a tarefa passada como parâmetro. Esse método não retorna nada (Unit) e lança uma NotFoundException caso a tarefa excluída não seja encontrada no banco de dados.
  • O método findById() busca a tarefa cujo ID é passado como parâmetro e retorna um Optional<Task> contendo a tarefa encontrada (ou um Optional vazio caso nenhuma tarefa seja encontrada).
  • O método save() salva a tarefa passada como parâmetro no banco de dados e retorna a tarefa salva.

O código-fonte da interface TaskRepository é o seguinte:

TaskRepository.kt
Arquivo Real KOTLIN
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.testwithspring.starter.mockk

import java.util.Optional

interface TaskRepository {

    fun count(): Long

    fun delete(task: Task)

    fun findById(id: Long): Optional<Task>

    fun save(task: Task): Task
}

Vamos avançar e descobrir como podemos configurar o comportamento dos métodos do nosso stub com o MockK (e seu paralelo com o BDDMockito).


Configurando o Comportamento de Métodos com o MockK

A primeira coisa que precisamos fazer é criar nosso stub. Como os dublês de teste criados com o MockK não são stateless, devemos criar um novo dublê de teste antes que cada método de teste seja invocado, utilizando um método @BeforeEach.

Depois de adicionarmos esse método de setup à nossa classe de teste e criarmos um stub de TaskRepository invocando stub(), o código-fonte inicial da nossa classe de teste fica assim:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.testwithspring.starter.mockk

import com.testwithspring.starter.mockk.TestDoubles.stub
import org.junit.jupiter.api.BeforeEach

class MockKStubTest {

    private lateinit var repository: TaskRepository

    @BeforeEach
    fun createStub() {
        repository = stub()
    }
}

Vamos agora configurar diferentes comportamentos em nossos stubs!


1. Retornando Valores e Objetos sem Parâmetros de Método

Se quisermos configurar o valor ou objeto retornado usando o MockK quando o método stubado não possui parâmetros, seguimos estes dois passos:

  1. Especificamos a chamada do método stubado dentro de um bloco every { ... } (do pacote io.mockk.every).
  2. Configuramos o valor ou objeto retornado encadeando a função infixa returns e passando o valor desejado.

Após configurarmos o método count() da interface TaskRepository para retornar 1L quando for invocado, o código-fonte do nosso método de teste fica assim:

 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
package com.testwithspring.starter.mockk

import com.testwithspring.starter.mockk.TestDoubles.stub
import io.mockk.every
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MockKStubTest {

    private lateinit var repository: TaskRepository

    @BeforeEach
    fun createStub() {
        repository = stub()
    }

    @Test
    fun `deve retornar valor configurado quando metodo nao tem parametros`() {
        every { repository.count() } returns 1L

        val actualCount = repository.count()

        assertThat(actualCount).isEqualByComparingTo(1L)
    }
}

[!NOTE] Paralelo com BDDMockito e Mockito clássico No BDDMockito, configurávamos essa mesma chamada com given(repository.count()).willReturn(1L). Na API original do Mockito, usávamos when(repository.count()).thenReturn(1L). No MockK, every { repository.count() } returns 1L é 100% seguro em tempo de compilação e lê-se como uma frase natural.


2. Retornando Valores e Objetos com Parâmetros de Método

Se quisermos configurar o valor ou objeto retornado quando o método stubado recebe parâmetros, seguimos estes passos:

  1. Especificamos a chamada do método stubado dentro do bloco every { ... }, passando o argumento esperado para o método.
  2. Configuramos o valor ou objeto retornado com returns.

Após configurarmos o método findById() da interface TaskRepository para retornar um Optional não vazio quando for invocado com o parâmetro 1L, o código-fonte do nosso método de teste fica assim:

 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
package com.testwithspring.starter.mockk

import com.testwithspring.starter.mockk.TestDoubles.dummy
import com.testwithspring.starter.mockk.TestDoubles.stub
import io.mockk.every
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.Optional

class MockKStubTest {

    private lateinit var repository: TaskRepository

    @BeforeEach
    fun createStub() {
        repository = stub()
    }

    @Test
    fun `deve retornar objeto configurado quando parametro esperado for passado`() {
        val expected: Task = dummy()
        every { repository.findById(1L) } returns Optional.of(expected)

        val actual = repository.findById(1L)

        assertThat(actual).containsSame(expected)
    }
}

3. Utilizando Argument Matchers (eq, any, ofType)

Quando passamos um valor diretamente para a chamada do método dentro de every { repository.findById(1L) }, o MockK (assim como o Mockito) verifica se o argumento real é igual ao esperado utilizando o método equals() (== em Kotlin).

Embora isso seja suficiente para a maioria das situações, às vezes precisamos de flexibilidade extra. Quando isso acontece, podemos especificar o parâmetro esperado utilizando um Argument Matcher passado como argumento para o método stubado.

O pacote io.mockk fornece diversas funções de argument matchers (equivalentes à classe org.mockito.ArgumentMatchers do Mockito). Vamos analisar dois exemplos que demonstram como eles funcionam:

Primeiro Exemplo: Matcher de igualdade explícita (eq())

Se quisermos configurar o método findById() da interface TaskRepository para retornar um Optional não vazio quando for invocado com um parâmetro igual a 1L usando um matcher explícito, usamos a função eq(1L) (equivalente a Matchers.eq(1L) no Mockito):

 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
package com.testwithspring.starter.mockk

import com.testwithspring.starter.mockk.TestDoubles.dummy
import com.testwithspring.starter.mockk.TestDoubles.stub
import io.mockk.every
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.Optional

class MockKStubTest {

    private lateinit var repository: TaskRepository

    @BeforeEach
    fun createStub() {
        repository = stub()
    }

    @Test
    fun `deve retornar objeto quando esperado for igual ao real usando eq`() {
        val expected: Task = dummy()
        every { repository.findById(eq(1L)) } returns Optional.of(expected)

        val actual = repository.findById(1L)

        assertThat(actual).containsSame(expected)
    }
}

Segundo Exemplo: Matcher para qualquer valor do tipo (any())

Se quisermos configurar o método findById() da interface TaskRepository para retornar um Optional não vazio quando for invocado com qualquer valor Long como parâmetro, utilizamos o matcher any() (ou any<Long>(), equivalente ao Matchers.anyLong() do Mockito):

 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
package com.testwithspring.starter.mockk

import com.testwithspring.starter.mockk.TestDoubles.dummy
import com.testwithspring.starter.mockk.TestDoubles.stub
import io.mockk.every
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.Optional

class MockKStubTest {

    private lateinit var repository: TaskRepository

    @BeforeEach
    fun createStub() {
        repository = stub()
    }

    @Test
    fun `deve retornar objeto quando parametro for qualquer Long usando any`() {
        val expected: Task = dummy()
        every { repository.findById(any()) } returns Optional.of(expected)

        val actual = repository.findById(1L)

        assertThat(actual).containsSame(expected)
    }
}

[!WARNING] Atenção ao misturar valores literais e Matchers No Mockito clássico, se um método possui múltiplos parâmetros e você usa um argument matcher em um deles, é obrigatório usar argument matchers (como eq(...)) em todos os demais parâmetros. O MockK é mais inteligente e frequentemente permite misturar literais com matchers, mas usar eq(...) explicitamente ao combinar múltiplos matchers no mesmo método continua sendo uma excelente prática de clareza!


4. Lançando Exceções (throws)

Se quisermos configurar uma exceção para ser lançada quando um método stubado for invocado, veja como o MockK simplifica nossa vida em comparação ao Mockito:

Primeiro Caso: Método que retorna valor (findById)

Para configurar o método findById() para lançar uma NotFoundException quando for invocado com o parâmetro 1L:

  1. Especificamos a chamada do método dentro de every { repository.findById(1L) }.
  2. Configuramos a exceção lançada encadeando throws e passando a instância da exceção (NotFoundException()).

(No BDDMockito, fazíamos given(repository.findById(1L)).willThrow(new NotFoundException()); no Mockito original, when(repository.findById(1L)).thenThrow(new NotFoundException())).

O código-fonte do nosso método de teste fica assim:

 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
package com.testwithspring.starter.mockk

import com.testwithspring.starter.mockk.TestDoubles.stub
import io.mockk.every
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class MockKStubTest {

    private lateinit var repository: TaskRepository

    @BeforeEach
    fun createStub() {
        repository = stub()
    }

    @Test
    fun `deve lancar excecao quando metodo possui valor de retorno`() {
        every { repository.findById(1L) } throws NotFoundException()

        assertThrows<NotFoundException> {
            repository.findById(1L)
        }
    }
}

[!CAUTION] Jamais quebre o contrato da sua API ao configurar um Stub! Observe que, pelo contrato real da interface TaskRepository, o método findById() não lança NotFoundException — quando a tarefa não é encontrada, ele retorna Optional.empty().

Usamos findById() acima apenas para demonstrar a sintaxe de throws em métodos com retorno. Em seus testes reais, nunca configure um stub para lançar uma exceção que o método real jamais lançaria, pois isso criaria um teste mentiroso que viola o contrato da sua API!

Segundo Caso: Método sem retorno (Unit / void — delete)

No Mockito clássico (Java), métodos void não podiam ser passados dentro de given(...) ou when(...) porque o compilador Java não aceita void como argumento de método! Isso obrigava o desenvolvedor a usar uma sintaxe invertida no Mockito:

  • BDDMockito: willThrow(new NotFoundException()).given(repository).delete(deleted);
  • Mockito: doThrow(new NotFoundException()).when(repository).delete(deleted);

No MockK em Kotlin, como every { ... } recebe um bloco lambda (e em Kotlin toda função sem retorno explícito retorna o objeto singleton Unit), a sintaxe é 100% idêntica para métodos com ou sem retorno!

Veja como configuramos o método delete() para lançar NotFoundException:

 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 com.testwithspring.starter.mockk.TestDoubles.dummy
import com.testwithspring.starter.mockk.TestDoubles.stub
import io.mockk.every
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class MockKStubTest {

    private lateinit var repository: TaskRepository

    @BeforeEach
    fun createStub() {
        repository = stub()
    }

    @Test
    fun `deve lancar excecao quando metodo nao possui valor de retorno`() {
        val deleted: Task = dummy()
        every { repository.delete(deleted) } throws NotFoundException()

        assertThrows<NotFoundException> {
            repository.delete(deleted)
        }
    }
}

5. Configurando uma Resposta Dinâmica (answers { ... })

Embora configurar o retorno fixo com returns seja suficiente para a maioria dos casos de uso, às vezes precisamos de mais poder — por exemplo, quando queremos acessar o objeto real que foi passado como parâmetro para o método stubado a fim de construir o objeto de retorno dinamicamente.

No Mockito clássico, resolvíamos isso instanciando uma classe anônima Answer<T> dentro de willAnswer(...) ou thenAnswer(...) e fazendo cast de invocation.getArguments()[0].

No MockK, fazemos isso de forma muito mais limpa encadeando um bloco answers { ... } logo após every { ... }! Dentro do bloco answers, temos acesso direto aos argumentos passados através de firstArg<T>(), secondArg<T>() ou arg<T>(index):

  1. Configuramos o método stubado com every { repository.findById(1L) }.
  2. Encadeamos o bloco answers { ... }.
  3. Acessamos o primeiro argumento como Long usando firstArg<Long>() (ou invocation.args[0] as Long) e retornamos Optional.of(expected) se o parâmetro for igual a 1L, ou Optional.empty() caso contrário.

Após configurarmos o comportamento do método findById() usando answers, o código-fonte do método de teste fica assim:

 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
31
32
33
34
35
36
package com.testwithspring.starter.mockk

import com.testwithspring.starter.mockk.TestDoubles.dummy
import com.testwithspring.starter.mockk.TestDoubles.stub
import io.mockk.every
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.Optional

class MockKStubTest {

    private lateinit var repository: TaskRepository

    @BeforeEach
    fun createStub() {
        repository = stub()
    }

    @Test
    fun `deve retornar objeto usando answers dinamico`() {
        val expected: Task = dummy()
        every { repository.findById(1L) } answers {
            val idParameter = firstArg<Long>()
            if (idParameter == 1L) {
                Optional.of(expected)
            } else {
                Optional.empty()
            }
        }

        val actual = repository.findById(1L)

        assertThat(actual).containsSame(expected)
    }
}

Colocando a Teoria em Prática

Agora vamos usar nossas novas habilidades para escrever um teste unitário real para o método createCopyOf() da classe TaskService. Esse método foi implementado seguindo estes seis passos:

  1. Verifica se ambos os parâmetros do método não são null.
  2. Busca a tarefa de origem no banco de dados (repository.findById(sourceTaskId)).
  3. Se a tarefa de origem não for encontrada, lança uma NotFoundException.
  4. Cria um novo objeto Task, copiando o título e a descrição da tarefa de origem e obtendo o ID do criador a partir do objeto LoggedInUser recebido como parâmetro.
  5. Salva o novo objeto Task criado no banco de dados (repository.save(created)).
  6. Retorna o objeto Task salvo.

O código-fonte completo da classe TaskService é o seguinte:

TaskService.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.testwithspring.starter.mockk

import org.slf4j.LoggerFactory

class TaskService(
    private val repository: TaskRepository? = null
) {

    companion object {
        private val LOGGER = LoggerFactory.getLogger(TaskService::class.java)
    }

    fun createCopyOf(sourceTaskId: Long?, creator: LoggedInUser?): Task {
        requireNotNull(sourceTaskId) { "sourceTaskId cannot be null" }
        requireNotNull(creator) { "creator cannot be null" }

        LOGGER.info(
            "Creating a copy of the task: {} with creator: {}",
            sourceTaskId,
            creator.username
        )

        if (repository == null) {
            return Task(id = sourceTaskId, creator = Creator(creator.userId))
        }

        val found = repository.findById(sourceTaskId)
        val source = found.orElseThrow { NotFoundException() }

        var created = Task.getBuilder()
            .withCreator(creator.userId)
            .withDescription(source.description)
            .withTitle(source.title)
            .build()

        created = repository.save(created)

        LOGGER.info("Created a new task: {}", created)

        return created
    }
}

Precisamos escrever um teste unitário que garanta que o método createCopyOf() retorna as informações da tarefa criada quando a tarefa de origem é encontrada no banco de dados. Para isso, configuramos o nosso stub TaskRepository em dois passos:

  1. Configuramos o stub TaskRepository para retornar a tarefa de origem (Optional.of(sourceTask)) quando seu método findById() for invocado com o ID SOURCE_TASK_ID.
  2. Configuramos o stub TaskRepository para retornar a tarefa salva com um novo ID (NEW_TASK_ID) quando seu método save() for invocado recebendo qualquer objeto do tipo Task (ofType(Task::class) ou any<Task>(), equivalente ao isA(Task.class) do Mockito), copiando os dados do argumento recebido via answers { ... }.

Após configurarmos o stub TaskRepository, o código-fonte completo da nossa classe de teste fica assim:

MockKTaskServiceTest.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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
package com.testwithspring.starter.mockk

import com.testwithspring.starter.mockk.TestDoubles.stub
import io.mockk.every
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
import java.util.Optional

@DisplayName("03 - Colocando Stubs em Prática com SUT")
class MockKTaskServiceTest {

    companion object {
        private const val CREATOR_ID = 33L
        private const val CREATOR_USERNAME = "username"
        private const val NEW_TASK_ID = 120L
        private const val SOURCE_TASK_ID = 99L
        private const val SOURCE_TASK_DESCRIPTION = "description"
        private const val SOURCE_TASK_TITLE = "title"
    }

    private lateinit var repository: TaskRepository
    private lateinit var service: TaskService

    @BeforeEach
    fun configureSystemUnderTest() {
        repository = stub()
        service = TaskService(repository)
    }

    @Test
    fun `createCopyOf quando tarefa de origem for encontrada deve retornar a tarefa criada`() {
        val sourceTask = TaskBuilder()
            .withDescription(SOURCE_TASK_DESCRIPTION)
            .withTitle(SOURCE_TASK_TITLE)
            .build()

        every { repository.findById(SOURCE_TASK_ID) } returns Optional.of(sourceTask)

        every { repository.save(ofType(Task::class)) } answers {
            val parameter = firstArg<Task>()
            TaskBuilder()
                .withId(NEW_TASK_ID)
                .withCreator(parameter.creator.userId)
                .withDescription(parameter.description)
                .withTitle(parameter.title)
                .build()
        }

        val creator = LoggedInUser(CREATOR_ID, CREATOR_USERNAME)
        val created = service.createCopyOf(SOURCE_TASK_ID, creator)

        assertThat(created.id).isEqualByComparingTo(NEW_TASK_ID)
        assertThat(created.creator.userId).isEqualByComparingTo(CREATOR_ID)
        assertThat(created.description).isEqualTo(SOURCE_TASK_DESCRIPTION)
        assertThat(created.title).isEqualTo(SOURCE_TASK_TITLE)
    }
}

Este exemplo prático demonstra perfeitamente por que o bloco answers { ... } (equivalente ao Answer<T> do Mockito) é tão útil: como o objeto Task passado para repository.save(created) é instanciado dentro do método createCopyOf(), a única maneira de o nosso stub simular o banco de dados atribuindo NEW_TASK_ID e preservando exatamente os campos (creator, description, title) que o TaskService preencheu na nova tarefa é inspecionando o parâmetro real firstArg<Task>() recebido pelo método save()!


Executando Nossos Testes no Gradle

Ao executarmos a suíte MockKTaskServiceTest através do Gradle (./gradlew test --tests MockKTaskServiceTest -i), inspecionamos a evidência real de colaboração entre o SUT e o Stub configurado via every:

Evidência Real de Execução: ./gradlew test --tests MockKTaskServiceTest -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 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.MockKStubTest > deve retornar valor configurado quando metodo nao tem parametros PASSED
com.testwithspring.starter.mockk.MockKStubTest > deve retornar objeto configurado quando parametro esperado for passado PASSED
com.testwithspring.starter.mockk.MockKStubTest > deve retornar objeto quando esperado for igual ao real usando eq PASSED
com.testwithspring.starter.mockk.MockKStubTest > deve retornar objeto quando parametro for qualquer Long usando any PASSED
com.testwithspring.starter.mockk.MockKStubTest > deve lancar excecao quando metodo possui valor de retorno PASSED
com.testwithspring.starter.mockk.MockKStubTest > deve lancar excecao quando metodo nao possui valor de retorno PASSED
com.testwithspring.starter.mockk.MockKStubTest > deve retornar objeto usando answers dinamico PASSED
com.testwithspring.starter.mockk.MockKTaskServiceTest > createCopyOf quando tarefa de origem for encontrada deve retornar a tarefa criada PASSED

MockK Stubs & SUT Collaboration > 8 tests completed, 0 failed, 0 skipped
Finished generating test XML results (0.012 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.018 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

Este exemplo prático demonstra perfeitamente por que o bloco answers { ... } (equivalente ao Answer<T> do Mockito) é tão útil: como o objeto Task passado para repository.save(created) é instanciado dentro do método createCopyOf(), a única maneira de o nosso stub simular o banco de dados atribuindo NEW_TASK_ID e preservando exatamente os campos (creator, description, title) que o TaskService preencheu na nova tarefa é inspecionando o parâmetro real firstArg<Task>() recebido pelo método save()!


Resumo

Nesta aula, aprendemos cinco pontos fundamentais:

  • Devemos criar nosso stub antes que cada método de teste seja invocado (@BeforeEach).
  • Configuramos o comportamento do nosso stub utilizando every { ... } returns ... ou every { ... } throws ... no MockK (equivalentes a given(...).willReturn(...) / when(...).thenReturn(...) no Mockito).
  • Por padrão, o MockK (assim como o Mockito) compara os parâmetros esperados com os parâmetros reais utilizando o método equals().
  • Podemos flexibilizar a verificação dos parâmetros do método utilizando argument matchers como eq(), any() e ofType().
  • Se precisarmos acessar o argumento real passado para o método stubado no momento de construir sua resposta, utilizamos o bloco answers { firstArg<T>() ... }.