Criando Mocks e Verificando Interações com verify

Ao concluirmos esta aula, nós:

  • Compreenderemos como criar nossos objetos Mock com o MockK.
  • Seremos capazes de verificar as interações que aconteceram (ou não aconteceram) entre o Sistema Sob Teste e um objeto Mock (verify, confirmVerified, Called).
  • Saberemos como capturar o parâmetro real passado para um método invocado utilizando slot<T>() (CapturingSlot, o equivalente ao ArgumentCaptor<T> do Mockito).

[!NOTE] Esta aula assume que:


Criando Mocks com o MockK

Como vimos nas aulas anteriores, podemos criar um dummy, um stub ou um mock invocando a função mockk() (ou Mockito.mock()).

Além disso, como os dublês de teste criados com o MockK não são stateless, devemos criar nossos dublês de teste antes que cada método de teste seja invocado, utilizando um método de setup anotado com @BeforeEach.

Desta vez, não precisamos adicionar nenhum método fábrica novo à classe TestDoubles, pois já é 100% cristalino que a função mockk() (ou mock()) cria um objeto Mock!

Depois de criarmos nosso objeto mock em um método @BeforeEach, 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 io.mockk.mockk
import org.junit.jupiter.api.BeforeEach

class MockKVerifyTest {

    private lateinit var repository: TaskRepository

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

[!NOTE] Mocks Estritos (strictMock) vs. Mocks Relaxados No Mockito clássico, Mockito.mock(TaskRepository::class.java) cria um mock relaxado por padrão; se você quisesse um strict mock no Mockito (que lança exceção ao receber chamadas não configuradas), adicionava um método strictMock() em TestDoubles:

1
2
3
object TestDoubles {
    inline fun <reified T : Any> strictMock(): T = mockk<T>(relaxed = false)
}

No MockK, mockk<TaskRepository>() sem argumentos já é estrito por padrão, enquanto mockk<TaskRepository>(relaxed = true) permite invocar métodos sem configuração prévia de every { ... } quando queremos focar exclusivamente na verificação posterior com verify { ... }.

Vamos descobrir como podemos verificar interações com o MockK!


Verificando Interações com o MockK (e o Paralelo com o Mockito)

O MockK fornece funções equivalentes aos três métodos estáticos clássicos de verificação do Mockito:

  1. verify { ... } (equivalente a Mockito.verify()): Verifica se uma interação específica aconteceu (ou quantas vezes aconteceu) entre o Sistema Sob Teste e o nosso objeto mock.
  2. confirmVerified(mock) (equivalente a Mockito.verifyNoMoreInteractions(mock)): Faz o teste falhar se qualquer um dos objetos mock passados como parâmetro possuir interações que não foram verificadas por uma chamada anterior de verify { ... }. Embora possa ser útil em situações pontuais, não devemos usar confirmVerified() indiscriminadamente em todos os métodos de teste para não torná-los frágeis.
  3. verify { mock wasNot Called } (equivalente a Mockito.verifyZeroInteractions(mock) / verifyNoInteractions(mock)): Verifica que nenhuma interação aconteceu entre o Sistema Sob Teste e o objeto mock especificado.

A seguir, veremos na prática como utilizar essas funções em nossos testes unitários!


Verificando Invocações de Métodos: 5 Exemplos Práticos

Os próximos cinco exemplos demonstram como verificar interações que aconteceram (ou que não aconteceram) entre o Sistema Sob Teste e o nosso objeto mock.

Exemplo 1: Verificar que o Método Foi Invocado Pelo Menos Uma Vez (ou Uma Vez)

Para verificar que o Sistema Sob Teste invocou o método findById() da interface TaskRepository passando 1L como parâmetro:

  1. Invocamos a função verify { ... } do MockK (ou verify(exactly = 1) { ... }).
  2. Dentro do bloco lambda de verify, fazemos a chamada ao método repository.findById(1L) com o parâmetro esperado.

(No Mockito, escrevíamos verify(repository).findById(1L)).

O código-fonte do nosso teste unitário 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
package com.testwithspring.starter.mockk

import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MockKVerifyTest {

    private lateinit var repository: TaskRepository

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

    @Test
    fun `deve verificar quando o metodo foi chamado uma vez`() {
        repository.findById(1L)

        verify { repository.findById(1L) }
    }
}

Exemplo 2: Verificar que o Método Foi Invocado Exatamente X Vezes (exactly = X)

Se quisermos verificar que o Sistema Sob Teste invocou um método do nosso objeto mock exatamente X vezes:

  • No Mockito, passávamos um VerificationMode como segundo argumento de verify: verify(repository, times(1)).findById(1L) (além de atLeast(n), atLeastOnce(), atMost(n)).
  • No MockK, passamos os parâmetros nomeados diretamente para a função verify:
    • verify(exactly = 1) { ... } (equivalente a times(1))
    • verify(atLeast = 1) { ... } (equivalente a atLeast(1) / atLeastOnce())
    • verify(atMost = 2) { ... } (equivalente a atMost(2))

Veja como fica o teste garantindo que findById(1L) foi invocado exatamente 1 vez:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.testwithspring.starter.mockk

import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MockKVerifyTest {

    private lateinit var repository: TaskRepository

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

    @Test
    fun `deve verificar que o metodo foi chamado exatamente uma vez`() {
        repository.findById(1L)

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

Exemplo 3: Verificar que o Método Nunca Foi Invocado (exactly = 0)

Se quisermos verificar que um método específico do nosso objeto mock não foi invocado pelo Sistema Sob Teste:

  • No Mockito, usávamos verify(repository, never()).findById(1L).
  • No MockK, usamos verify(exactly = 0) { repository.findById(1L) }!

Se quisermos garantir que o método findById() da interface TaskRepository jamais foi invocado com o parâmetro 1L, escrevemos o seguinte teste unitário:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.testwithspring.starter.mockk

import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MockKVerifyTest {

    private lateinit var repository: TaskRepository

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

    @Test
    fun `deve verificar que o metodo nunca foi invocado`() {
        verify(exactly = 0) { repository.findById(1L) }
    }
}

Exemplo 4: Verificar que Não Há Interações Não Verificadas (confirmVerified)

Se quisermos verificar que o nosso mock não recebeu nenhuma outra chamada além daquelas que já verificamos explicitamente, seguimos estes dois passos:

  1. Especificamos as interações esperadas com verify { ... }.
  2. Invocamos a função confirmVerified(repository) do MockK (equivalente a verifyNoMoreInteractions(repository) do Mockito) passando o mock TaskRepository como parâmetro.

Por exemplo, para verificar que o Sistema Sob Teste invocou findById(1L) uma vez e não invocou nenhum outro método da interface TaskRepository:

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

import io.mockk.confirmVerified
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MockKVerifyTest {

    private lateinit var repository: TaskRepository

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

    @Test
    fun `deve verificar que nenhum outro metodo foi invocado`() {
        repository.findById(1L)

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

Exemplo 5: Verificar que Não Houve Nenhuma Interação com o Mock (wasNot Called)

Se quisermos verificar que o Sistema Sob Teste não invocou absolutamente nenhum método da interface TaskRepository, usamos verify { repository wasNot Called } (ou confirmVerified(repository) sem verify anterior, equivalente ao verifyZeroInteractions(repository) do Mockito):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.testwithspring.starter.mockk

import io.mockk.Called
import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MockKVerifyTest {

    private lateinit var repository: TaskRepository

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

    @Test
    fun `deve verificar que nenhuma interacao ocorreu entre o SUT e o mock`() {
        verify { repository wasNot Called }
    }
}

Utilizando Argument Matchers na Verificação

Assim como vimos ao configurar stubs, quando passamos um valor diretamente para o método dentro de verify { repository.findById(1L) }, o MockK compara o argumento esperado com o argumento real utilizando o método equals(). Quando precisamos de flexibilidade adicional, usamos Argument Matchers:

Primeiro Exemplo: Usando eq()

Se quisermos garantir que o método findById() foi invocado com um parâmetro igual a 1L usando o matcher eq(1L):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.testwithspring.starter.mockk

import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MockKVerifyTest {

    private lateinit var repository: TaskRepository

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

    @Test
    fun `deve verificar que o metodo foi invocado passando 1L com eq`() {
        repository.findById(1L)

        verify { repository.findById(eq(1L)) }
    }
}

Segundo Exemplo: Usando any()

Se quisermos verificar que o método findById() foi invocado passando qualquer valor Long como parâmetro, usamos o matcher any() (equivalente a anyLong() 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
package com.testwithspring.starter.mockk

import io.mockk.mockk
import io.mockk.verify
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MockKVerifyTest {

    private lateinit var repository: TaskRepository

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

    @Test
    fun `deve verificar que o metodo foi invocado passando qualquer Long com any`() {
        repository.findById(1L)

        verify { repository.findById(any()) }
    }
}

Capturando Argumentos Reais com CapturingSlot (slot<T>())

Se quisermos escrever asserções detalhadas (com o AssertJ) sobre o parâmetro real que o Sistema Sob Teste passou para o nosso mock, os argument matchers simples (eq, any) não bastam — precisamos capturar o parâmetro real antes de escrever nossas asserções sobre ele!

  • No Mockito, capturamos o parâmetro utilizando a classe ArgumentCaptor<T> (ArgumentCaptor.forClass(T::class.java), argument.capture() e argument.value).
  • No MockK, capturamos o parâmetro utilizando um CapturingSlot<T>, criado com a função slot<T>()!

Por exemplo, se quisermos capturar o parâmetro passado para o método findById() da interface TaskRepository e verificar que ele é igual a 1L, seguimos estes quatro passos:

  1. Criamos um objeto CapturingSlot<Long> invocando a função slot<Long>() do pacote io.mockk.slot.
  2. Garantimos que o método findById() foi invocado uma vez e capturamos o parâmetro passado invocando capture(argument) dentro de verify.
  3. Obtemos o parâmetro real capturado acessando a propriedade argument.captured (equivalente a argument.getValue() no ArgumentCaptor do Mockito).
  4. Escrevemos uma asserção com o AssertJ garantindo que o valor capturado é igual a 1L.

Após concluirmos esses passos, 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 io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test

class MockKVerifyTest {

    private lateinit var repository: TaskRepository

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

    @Test
    fun `deve verificar que o metodo foi invocado capturando o argumento com slot`() {
        repository.findById(1L)

        val argument = slot<Long>()
        verify(exactly = 1) { repository.findById(capture(argument)) }

        val actualId = argument.captured
        assertThat(actualId).isEqualByComparingTo(1L)
    }
}

Agora já sabemos como verificar interações e capturar argumentos entre o Sistema Sob Teste e nosso objeto mock. Vamos colocar nossas novas habilidades em prática!


Colocando a Teoria em Prática

Vamos usar nossas novas habilidades para escrever um teste unitário para o método createCopyOf() da classe TaskService (que conhecemos na aula sobre Stubs). Desta vez, precisamos escrever um teste unitário que garanta que o objeto Task salvo no banco de dados (repository.save(...)) contém as informações corretas.

Podemos escrever esse teste unitário seguindo estes quatro passos:

  1. Criamos um novo objeto slot<Task>() (equivalente a ArgumentCaptor<Task>).
  2. Garantimos que o método save() da interface TaskRepository foi invocado uma vez e capturamos o objeto Task real passado como parâmetro com capture(taskArgument).
  3. Obtemos o objeto Task capturado com taskArgument.captured.
  4. Verificamos com o AssertJ se o objeto Task salvo contém o ID do criador, a descrição e o título corretos!

Após finalizarmos nosso teste unitário, o código-fonte completo 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
40
41
42
43
44
45
46
47
48
49
50
51
package com.testwithspring.starter.mockk

import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.verify
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.Optional

class MockKTaskServiceVerifyTest {

    companion object {
        private const val CREATOR_ID = 33L
        private const val CREATOR_USERNAME = "username"
        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 = mockk(relaxed = true)
        service = TaskService(repository)
    }

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

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

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

        val taskArgument = slot<Task>()
        verify(exactly = 1) { repository.save(capture(taskArgument)) }

        val created = taskArgument.captured
        assertThat(created.creator.userId).isEqualByComparingTo(CREATOR_ID)
        assertThat(created.description).isEqualTo(SOURCE_TASK_DESCRIPTION)
        assertThat(created.title).isEqualTo(SOURCE_TASK_TITLE)
    }
}

Resumo

Nesta aula, aprendemos seis pontos fundamentais:

  • Devemos criar nosso objeto mock antes que cada método de teste seja invocado (@BeforeEach).
  • Criamos nosso objeto mock utilizando a função mockk() (ou Mockito.mock()).
  • Verificamos as interações que aconteceram (ou não aconteceram) entre o Sistema Sob Teste e um objeto mock utilizando verify { ... }, verify(exactly = N) { ... }, confirmVerified(mock) e verify { mock wasNot Called }.
  • Por padrão, o MockK compara o parâmetro esperado com o parâmetro real utilizando o método equals().
  • Podemos flexibilizar a verificação dos parâmetros utilizando argument matchers (eq(), any()).
  • Se precisarmos obter uma referência para o objeto real que foi passado como parâmetro para o método do mock a fim de inspecioná-lo com o AssertJ, utilizamos slot<T>() + capture(slot) (o equivalente ao ArgumentCaptor<T> do Mockito).