Verificando Invocações e Slots no MockK

Ao concluirmos esta aula, compreenderemos como podemos verificar as interações que aconteceram entre o sistema sob teste (SUT) e um mock utilizando MockK 1.14, Kotlin 2.4.21 e JUnit Jupiter 6.0.3.

Este webinar assume que:

Obtenha o código-fonte no GitHub

Introdução ao Sistema Sob Teste (SUT)

Durante esta aula, verificaremos as interações que aconteceram entre o sistema sob teste e um mock de TaskRepository. A interface TaskRepository declara as funções que fornecem operações CRUD para objetos Task.

Como o objetivo é manter tudo o mais simples e direto possível, adicionamos as declarações dos componentes do nosso modelo de dados em um único arquivo de código-fonte chamado Task.kt. Esse arquivo tem a seguinte estrutura:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
data class Creator(val id: Long)

enum class TaskResolution {
    DUPLICATE,
    DONE,
    WONT_DO
}

enum class TaskStatus {
    CLOSED,
    IN_PROGRESS,
    OPEN
}

data class Task(
    val id: Long?,
    val creator: Creator,
    val title: String,
    val status: TaskStatus,
    val resolution: TaskResolution?
)

A interface TaskRepository possui três funções:

  • A função findById() retorna as informações de uma tarefa utilizando o seu id como critério de busca. Se nenhuma tarefa for encontrada no banco de dados, esta função retorna null.
  • A função findByCreatorAndStatus() retorna todas as tarefas que foram criadas pela pessoa especificada e que possuem o status informado. Se nenhuma tarefa for encontrada no banco de dados, esta função retorna uma lista vazia.
  • A função create() insere uma nova tarefa no banco de dados e retorna as informações da tarefa criada.

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

1
2
3
4
5
6
7
8
interface TaskRepository {

    fun findById(id: Long): Task?

    fun findByCreatorAndStatus(creatorId: Long, status: TaskStatus): List<Task>

    fun create(task: Task): Task
}

A seguir, aprenderemos a verificar invocações de função com o MockK.

Verificando Invocações de Função com o MockK

Quando precisamos verificar as interações que aconteceram entre o sistema sob teste e um mock no MockK 1.14, podemos utilizar as seguintes funções:

  • A função verify() nos permite verificar que o sistema sob teste invocou a função especificada X vezes utilizando os parâmetros de função esperados. Podemos especificar o número esperado de invocações de função utilizando estes parâmetros:
    • O parâmetro atLeast nos permite verificar que a função especificada foi invocada pelo menos X vezes.
    • O parâmetro atMost nos ajuda a garantir que a função especificada foi invocada no máximo X vezes.
    • O parâmetro exactly nos permite especificar o número exato de invocações esperadas da função.
  • A função confirmVerified() nos ajuda a garantir que a nossa função de teste verifique todas as interações ocorridas entre o sistema sob teste e os objetos mock especificados. Esta função é tipicamente utilizada em conjunto com a função verify().
  • A função verifyAll() verifica todas as invocações de função especificadas. Esta função ignora a ordem das invocações de função.
  • A função verifyOrder() garante que as invocações de função especificadas aconteceram na ordem esperada.
  • A função verifySequence() verifica que as invocações de função especificadas aconteceram na ordem esperada e garante que não houve nenhuma outra interação entre o sistema sob teste e o nosso mock.

Leitura Adicional:

Vamos analisar oito exemplos que demonstram como podemos verificar as interações que aconteceram entre o sistema sob teste e um mock.

Exemplo 1: Verificando que uma Função Não Foi Invocada

Se precisarmos verificar que uma função não foi invocada com os parâmetros especificados, devemos utilizar a função verify(). Quando invocamos esta função, precisamos especificar o número esperado de invocações configurando o valor do parâmetro exactly para 0. Além disso, devemos especificar a invocação de função que não deveria ter acontecido.

Vamos verificar que a função create() da interface TaskRepository não foi invocada. Depois de especificarmos a nossa expectativa, o código-fonte da nossa função de teste fica 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
31
32
33
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("unitTest")
@DisplayName("Verify interactions between SUT and a mock")
class TaskRepositoryMockTest {

    companion object {

        private const val CREATOR_ID = 5L
        private const val TASK_ID = 1L
        private const val TITLE = "Example task"
    }

    private lateinit var repository: TaskRepository

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

    @Test
    @DisplayName("Should verify that the create() function was not invoked")
    fun shouldVerifyThatCreateFunctionWasNotInvoked() {
        repository.findById(TASK_ID)

        verify(exactly = 0) { repository.create(any()) }
    }
}

Exemplo 2: Verificando que uma Função Foi Invocada Uma Vez

Se precisarmos verificar que uma função foi invocada uma única vez utilizando os parâmetros de função especificados, devemos usar a função verify(). Quando invocamos esta função, precisamos especificar o número esperado de invocações definindo o valor do parâmetro exactly como 1. Também devemos especificar a função invocada e os parâmetros esperados.

Vamos verificar que a função findById() da interface TaskRepository foi invocada uma vez utilizando o parâmetro de função 1L. Depois de especificarmos a nossa expectativa, o código-fonte da nossa função 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
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("unitTest")
@DisplayName("Verify interactions between SUT and a mock")
class TaskRepositoryMockTest {

    companion object {

        private const val CREATOR_ID = 5L
        private const val TASK_ID = 1L
        private const val TITLE = "Example task"
    }

    private lateinit var repository: TaskRepository

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

    @Test
    @DisplayName("Should verify that findById() function was invoked once")
    fun shouldVerifyThatFindByIdFunctionWasInvokedOnce() {
        repository.findById(TASK_ID)

        verify(exactly = 1) { repository.findById(TASK_ID) }
    }
}

Exemplo 3: Verificando que Não Existem Invocações de Função Não Verificadas

Se precisarmos verificar que não restaram invocações de função não verificadas, devemos utilizar a função confirmVerified(). Ao invocarmos esta função, devemos passar os objetos mock inspecionados como argumento da função.

Vamos garantir que a nossa função de teste invocou apenas a função findById() da interface TaskRepository utilizando o parâmetro de função 1L. Após especificarmos a nossa expectativa, o código-fonte da nossa função de teste fica 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
31
32
33
34
35
import io.mockk.confirmVerified
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("unitTest")
@DisplayName("Verify interactions between SUT and a mock")
class TaskRepositoryMockTest {

    companion object {

        private const val CREATOR_ID = 5L
        private const val TASK_ID = 1L
        private const val TITLE = "Example task"
    }

    private lateinit var repository: TaskRepository

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

    @Test
    @DisplayName("Should verify that there are no unverified invocations")
    fun shouldVerifyThatThereAreNoUnverifiedInvocations() {
        repository.findById(TASK_ID)

        verify(exactly = 1) { repository.findById(TASK_ID) }
        confirmVerified(repository)
    }
}

Exemplo 4: Verificando que Apenas as Invocações de Função Especificadas Aconteceram em Qualquer Ordem

Se precisarmos verificar que apenas as invocações de função especificadas aconteceram, independentemente da ordem em que foram chamadas, devemos especificar as invocações de função esperadas utilizando a função verifyAll().

Vamos verificar que a nossa função de teste invocou a função findById() duas vezes utilizando os parâmetros de função 1L e 5L. Depois de especificarmos a nossa expectativa, o código-fonte da nossa função de teste fica da seguinte maneira:

 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
import io.mockk.mockk
import io.mockk.verifyAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("unitTest")
@DisplayName("Verify interactions between SUT and a mock")
class TaskRepositoryMockTest {

    companion object {

        private const val CREATOR_ID = 5L
        private const val TASK_ID = 1L
        private const val TITLE = "Example task"
    }

    private lateinit var repository: TaskRepository

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

    @Test
    @DisplayName("Should verify invocations but ignore order")
    fun shouldVerifyInvocationsButIgnoreOrder() {
        repository.findById(5L)
        repository.findById(TASK_ID)

        verifyAll {
            repository.findById(TASK_ID)
            repository.findById(5L)
        }
    }
}

Exemplo 5: Verificando que Apenas as Invocações de Função Especificadas Aconteceram na Ordem Informada

Se precisarmos verificar que apenas as invocações de função especificadas aconteceram exatamente na ordem informada (sem nenhuma outra interação adicional), devemos especificar as invocações esperadas utilizando a função verifySequence().

Vamos verificar que a nossa função de teste invocou a função findById() três vezes utilizando os parâmetros de função 5L, 1L e 99L. Após especificarmos a nossa expectativa, o código-fonte da nossa função 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
37
38
39
import io.mockk.mockk
import io.mockk.verifySequence
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("unitTest")
@DisplayName("Verify interactions between SUT and a mock")
class TaskRepositoryMockTest {

    companion object {

        private const val CREATOR_ID = 5L
        private const val TASK_ID = 1L
        private const val TITLE = "Example task"
    }

    private lateinit var repository: TaskRepository

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

    @Test
    @DisplayName("Should verify a sequence of function invocations")
    fun shouldVerifySequenceOfFunctionInvocations() {
        repository.findById(5L)
        repository.findById(TASK_ID)
        repository.findById(99L)

        verifySequence {
            repository.findById(5L)
            repository.findById(TASK_ID)
            repository.findById(99L)
        }
    }
}

Exemplo 6: Verificando que as Invocações de Função Especificadas Aconteceram na Ordem Informada

Se precisarmos verificar que determinadas invocações de função aconteceram na ordem informada (permitindo que outras invocações tenham ocorrido entre elas), devemos especificar as invocações esperadas utilizando a função verifyOrder().

Vamos verificar que a nossa função de teste invocou a função findById() duas vezes na ordem esperada utilizando os parâmetros de função 5L e 99L. Depois de especificarmos a nossa expectativa, o código-fonte da nossa função de teste fica 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
31
32
33
34
35
36
37
38
import io.mockk.mockk
import io.mockk.verifyOrder
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("unitTest")
@DisplayName("Verify interactions between SUT and a mock")
class TaskRepositoryMockTest {

    companion object {

        private const val CREATOR_ID = 5L
        private const val TASK_ID = 1L
        private const val TITLE = "Example task"
    }

    private lateinit var repository: TaskRepository

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

    @Test
    @DisplayName("Should verify the order of function invocations")
    fun shouldVerifyOrderOfFunctionInvocations() {
        repository.findById(5L)
        repository.findById(TASK_ID)
        repository.findById(99L)

        verifyOrder {
            repository.findById(5L)
            repository.findById(99L)
        }
    }
}

Exemplo 7: Utilizando Argument Matchers Simples

Se quisermos especificar os parâmetros de função esperados utilizando argument matchers simples, podemos empregar esses matchers diretamente quando declararmos a invocação de função esperada dentro do bloco de verificação.

Por exemplo, vamos verificar que a nossa função de teste invocou a função findByCreatorAndStatus() da interface TaskRepository uma vez utilizando como parâmetros de função: qualquer objeto Long (any()) e TaskStatus.OPEN.

Depois de especificarmos a nossa expectativa, o código-fonte da nossa função de teste fica 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
31
32
33
34
35
36
37
38
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("unitTest")
@DisplayName("Verify interactions between SUT and a mock")
class TaskRepositoryMockTest {

    companion object {

        private const val CREATOR_ID = 5L
        private const val TASK_ID = 1L
        private const val TITLE = "Example task"
    }

    private lateinit var repository: TaskRepository

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

    @Test
    @DisplayName("Should use a simple argument matcher")
    fun shouldUseSimpleArgumentMatcher() {
        repository.findByCreatorAndStatus(1L, TaskStatus.OPEN)

        verify(exactly = 1) {
            repository.findByCreatorAndStatus(
                any(),
                TaskStatus.OPEN
            )
        }
    }
}

Exemplo 8: Especificando o Parâmetro de Função Esperado com um Predicado

Se quisermos especificar o parâmetro de função esperado utilizando um predicado, devemos definir o predicado por meio da função match() ao declararmos a invocação de função esperada.

Vamos verificar que a nossa função de teste invocou a função create() da interface TaskRepository uma única vez e garantir que a propriedade id do objeto Task passado como parâmetro de função seja igual a 1L.

Depois de especificarmos a nossa expectativa, o código-fonte da nossa função de teste fica da seguinte maneira:

 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
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("unitTest")
@DisplayName("Verify interactions between SUT and a mock")
class TaskRepositoryMockTest {

    companion object {

        private const val CREATOR_ID = 5L
        private const val TASK_ID = 1L
        private const val TITLE = "Example task"
    }

    private lateinit var repository: TaskRepository

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

    @Test
    @DisplayName("Should ensure that argument fulfills the given predicate")
    fun shouldEnsureThatArgumentFulfillsPredicate() {
        repository.create(
            Task(
                id = TASK_ID,
                creator = Creator(id = CREATOR_ID),
                title = TITLE,
                status = TaskStatus.OPEN,
                resolution = null
            )
        )

        verify(exactly = 1) { repository.create(match { it.id == TASK_ID }) }
    }
}

Agora já sabemos como verificar as interações que aconteceram entre o sistema sob teste e um mock. Vamos resumir o que aprendemos nesta aula.

Resumo

Esta aula nos ensinou cinco coisas fundamentais:

  • A função verify() nos permite verificar que o sistema sob teste invocou a função especificada X vezes utilizando os parâmetros de função esperados.
  • A função confirmVerified() nos ajuda a garantir que a nossa função de teste verifique todas as interações entre o sistema sob teste e os objetos mock especificados.
  • A função verifyAll() verifica todas as invocações de função especificadas e ignora a ordem das invocações de função.
  • A função verifyOrder() garante que as invocações de função especificadas aconteceram na ordem esperada.
  • A função verifySequence() verifica que as invocações de função especificadas aconteceram na ordem esperada e garante que não houve nenhuma outra interação entre o sistema sob teste e o nosso mock.

Obtenha o código-fonte no GitHub