Verificando Interações em Especificações BDD
Depois de concluirmos esta aula, nós:
- Saberemos como criar e isolar objetos mock com MockK 1.14 dentro de especificações do Kotest 6.
- Compreenderemos como verificar as interações que ocorreram entre o sistema sob especificação e um mock (
verify(exactly = ...),atLeast,atMost). - Saberemos como combinar o stubbing do retorno e a verificação da interação de um método no mesmo cenário BDD.
1. Criando Mocks com MockK em Especificações do Kotest
No Spock Framework, criávamos um objeto mock invocando o método Mock(MessageRepository). No ecossistema Kotlin com Kotest 6 + MockK 1.14, criamos mocks utilizando a função mockk<MessageRepository>() (ou mockk<MessageRepository>(relaxed = true) se quisermos que chamadas a métodos cujos retornos não foram configurados retornem valores padrão automaticamente, exatamente como o Mock() do Spock faz!).
Assim como vimos para os stubs, devemos garantir que cada caso de teste da especificação tenha seu próprio mock limpo para que interações registradas em um teste não interfiram no próximo. Conseguimos isso configurando isolationMode = IsolationMode.InstancePerLeaf (ou limpando o mock com clearMocks(repository) no beforeEach):
|
|
[!NOTE] Três observações rápidas antes de prosseguirmos:
- O sistema sob especificação (
MessageService,MessageRepositoryeMessage) é o mesmo utilizado nas duas aulas anteriores.- Se a construção do sistema sob especificação exigir mais de uma linha de configuração, você pode inicializá-lo dentro de um callback
beforeEach { ... }.- O MockK consegue criar mocks tanto de interfaces quanto de classes concretas (
finalpor padrão em Kotlin),objects singletons e classes enumeradas sem exigir plugins extras no build!
Agora que configuramos nosso mock, vamos ver como verificar as interações que aconteceram entre o MessageService e o MessageRepository.
2. Verificando Interações com verify
Para verificar as interações ocorridas entre o sistema sob especificação e um mock usando o MockK dentro do bloco Then do Kotest, seguimos quatro passos:
- Especificamos a cardinalidade (número esperado de chamadas) nos parâmetros do bloco
verify(...)— pode ser um número fixo (exactly = N) ou um intervalo (atLeast = X, atMost = Y). - Identificamos o objeto mock cujo método deve (ou não deve) ter sido invocado.
- Identificamos o método invocado usando a sintaxe normal de chamada em Kotlin.
- Especificamos os parâmetros esperados usando valores literais ou argument matchers.
Vamos examinar sete exemplos que demonstram todas as variações de verificação de interações equivalentes ao Spock!
2.1. Verificando que um Método NÃO Foi Invocado (exactly = 0)
Vamos garantir que o método findById() do mock MessageRepository não foi invocado (equivalente a 0 * repository.findById(_) no Spock) quando criamos uma nova mensagem:
|
|
[!TIP] Se você quiser garantir que o mock
repositorynão sofreu nenhuma interação em nenhum de seus métodos, pode usarverify { repository wasNot Called }.
2.2. Verificando que um Método Foi Invocado Exatamente Uma Vez (exactly = 1)
Vamos verificar que o método save() do mock MessageRepository foi invocado exatamente uma vez passando o objeto message (equivalente a 1 * repository.save(message)):
|
|
2.3. Verificando que um Método Foi Invocado Entre X e Y Vezes (atLeast = X, atMost = Y)
Se quisermos garantir que um método do mock foi invocado entre X e Y vezes (equivalente a (0..2) * repository.save(message) no Spock), combinamos os parâmetros atLeast e atMost no verify:
|
|
2.4. Verificando que um Método Foi Invocado Pelo Menos X Vezes (atLeast = X)
Para verificar que um método foi invocado pelo menos X vezes (equivalente a (1.._) * repository.save(message)), usamos verify(atLeast = 1) (que também é o comportamento padrão de verify { ... } quando nenhum parâmetro de cardinalidade é informado):
|
|
2.5. Verificando que um Método Foi Invocado No Máximo X Vezes (atMost = X)
Para garantir que um método foi invocado no máximo X vezes (equivalente a (_..1) * repository.save(message)), utilizamos verify(atMost = 1):
|
|
2.6. Permitindo Qualquer Número de Invocações, Incluindo Zero (atLeast = 0)
Se quisermos declarar que um método pode ter sido invocado qualquer número de vezes, inclusive zero (equivalente a _ * repository.save(message), útil quando usamos confirmVerified(repository) no final do teste e queremos marcar uma chamada opcional como reconhecida), usamos verify(atLeast = 0):
|
|
2.7. Usando Restrições de Argumentos na Verificação (match / withArg)
Assim como no stubbing, podemos usar todos os argument matchers do MockK dentro do bloco verify! Vamos garantir que o método save() foi invocado exatamente uma vez recebendo uma Message cujo atributo messageText seja igual a "Hello World!":
|
|
[!TIP] Se você preferir usar os matchers ricos do próprio Kotest (
shouldBe) para inspecionar o argumento capturado pelo MockK, pode usarwithArg { ... }dentro doverify:
1 2 3 4 5verify(exactly = 1) { repository.save(withArg { saved -> saved.messageText shouldBe "Hello World!" }) }Se
saved.messageTextdivergir, o Kotest lançará umAssertionErrordetalhado mostrando o valor esperado versus o valor real!
3. Combinando Stubbing e Verificação de Interação
Muitas vezes, queremos ao mesmo tempo:
- Configurar a resposta do método quando ele for chamado pelo sistema sob especificação (stubbing).
- Verificar que a interação realmente ocorreu com os parâmetros esperados (mocking).
No Spock, por conta de sua transformação AST, éramos obrigados a juntar a verificação e o stubbing na mesma linha dentro do bloco then: (1 * repository.save(message) >> { ... }), o que frequentemente confundia a separação temporal entre preparação e verificação.
No Kotest + MockK, mantemos a separação semântica cristalina do BDD:
- Configuramos o comportamento do stub no bloco
Given(ouAndde preparação) usandoevery { ... } answers { ... }. - Executamos a ação no bloco
When. - Verificamos a interação com
verify(exactly = 1)e o objeto retornado no blocoThen!
Veja como fica nosso cenário completo para o método create():
|
|
Resumo
Nesta aula, aprendemos cinco pontos fundamentais:
- Criamos mocks em especificações do Kotest usando
mockk<T>()(oumockk<T>(relaxed = true)). - Garantimos que cada teste receba seu próprio mock isolado usando
IsolationMode.InstancePerLeaf. - Especificamos a cardinalidade esperada das interações no
verifyusandoexactly,atLeasteatMost. - Podemos inspecionar os argumentos passados ao mock usando
match { ... }ou combinandowithArg { ... }com as asserções do Kotest. - Quando precisamos configurar o retorno e verificar a chamada do mesmo método, configuramos o
every { ... }no blocoGivene verificamos overify(exactly = 1) { ... }no blocoThen.