Criando Stubs em Kotest com Matchers de Argumentos
Depois de concluirmos esta aula, nós:
- Saberemos como criar e isolar stubs usando MockK 1.14 dentro de especificações do Kotest 6.
- Saberemos como configurar os parâmetros esperados de um método stubbado utilizando todas as restrições de argumentos (argument matchers) do MockK: igualdade exata, desigualdade (
not), qualquer argumento incluindo nulo (anyNullable), qualquer lista de argumentos, qualquer argumento não nulo de um tipo específico (any<Long>()) e predicados customizados (match { ... }).
1. Conhecendo o Sistema Sob Especificação (SUS)
Durante esta aula, criaremos um stub para a interface MessageRepository e o utilizaremos para especificar o comportamento esperado da classe MessageService.
A interface MessageRepository declara um método chamado findById(), que recebe o id da mensagem como parâmetro e retorna um Optional<Message> contendo a mensagem encontrada (ou Optional.empty() caso nenhuma mensagem seja encontrada):
|
|
A classe Message armazena os dados de uma única mensagem:
|
|
A classe MessageService recebe MessageRepository via construtor e possui um método findById(id: Long?) que consulta o repositório e retorna a Message encontrada ou lança NotFoundException se o Optional estiver vazio:
|
|
Vamos ver agora como criar e isolar stubs com MockK dentro das nossas especificações do Kotest.
2. Criando e Isolando Stubs no Kotest com MockK
No Spock Framework, criávamos um stub invocando Stub(MessageRepository) (que por padrão aceita chamadas inesperadas retornando valores vazios/padrão).
No ecossistema moderno Kotlin com Kotest 6 + MockK 1.14, temos duas maneiras de criar um stub:
mockk<MessageRepository>()(Estrito por padrão): exige que você configure explicitamente (every { ... } returns ...) os métodos que serão chamados durante o teste. Se o sistema sob especificação chamar um método não configurado, o MockK falha imediatamente avisando qual chamada faltou configurar.mockk<MessageRepository>(relaxed = true)(Relaxado — comportamento idêntico aoStub()do Spock): permite chamadas não configuradas e retorna automaticamente valores padrão (como0para números,""paraString, coleções vazias ou objetos dummy).
[!IMPORTANT] Isolamento de Stubs entre Casos de Teste no Kotest
Quando criamos stubs em uma especificação do Kotest, devemos garantir que a configuração (
every { ... }) feita em um caso de teste nunca vaze para os demais casos de teste da mesma classe! Temos duas maneiras simples de garantir isso:
- Configurar
isolationMode = IsolationMode.InstancePerLeafna especificação (assim cada teste ganha uma instância novinha derepositoryeservice).- Ou chamar
clearMocks(repository)dentro de um blocobeforeEach { ... }.
Vamos criar nossa classe de especificação MessageServiceSpec configurando o stub repository e o serviço service:
|
|
Agora que configuramos nosso sistema sob especificação, vamos explorar todas as restrições de argumentos (argument constraints / matchers) para identificar o método stubbado e seus parâmetros esperados!
3. Usando Restrições de Argumentos (Argument Constraints)
No MockK, identificamos o método stubbado dentro de um bloco every { ... } usando a sintaxe normal de chamada de método do Kotlin e especificamos os parâmetros esperados usando valores literais ou argument matchers.
Vamos analisar todos os 7 tipos de restrições de argumentos equivalentes às restrições do Spock.
3.1. Argumento Igual a um Valor (eq ou Valor Literal)
Se quisermos garantir que o método findById() do MessageRepository seja invocado com um parâmetro exatamente igual a 1L, basta passar 1L (ou eq(1L)) diretamente na chamada dentro do every:
|
|
[!NOTE] Na próxima aula deste tópico, estudaremos em detalhes todas as variações de resposta (
returns,returnsMany,answers,throws). Aqui usamosreturns Optional.of(found)para retornar um valor fixo (equivalente ao operador>>do Spock).
3.2. Argumento Diferente de um Valor (not / neq)
Se quisermos configurar o stub para responder quando o parâmetro passado não for igual a 2L (equivalente ao !2L do Spock), usamos o matcher not(2L) (ou neq(2L)) do MockK:
|
|
3.3. Qualquer Argumento Único, Incluindo null (anyNullable())
No Spock, o caractere sublinhado _ aceitava qualquer argumento único, inclusive null.
No MockK, como o Kotlin possui null-safety no sistema de tipos, existem dois matchers curinga:
any(): aceita qualquer argumento não nulo do tipo esperado.anyNullable()(ouallAny()): aceita qualquer argumento, incluindonull!
Vamos configurar nosso stub para retornar a mensagem encontrada mesmo quando service.findById(null) for invocado:
|
|
3.4. Qualquer Lista de Argumentos / Varargs (anyVararg() / allAny())
Se o método possuir múltiplos parâmetros ou um vararg e quisermos aceitar qualquer combinação de argumentos (equivalente ao *_ do Spock), podemos usar allAny() nos parâmetros ou anyVararg() para arrays variáveis:
|
|
3.5. Argumento Não Nulo (not(isNull()) ou any())
Se quisermos garantir explicitamente que o método findById() só retorne a mensagem quando o argumento passado não for null (equivalente ao !null do Spock), usamos not(isNull()) (ou simplesmente any() para tipos não nulos):
|
|
3.6. Argumento Não Nulo de um Tipo Específico (any<Long>() / ofType(Long::class))
Para exigir que o parâmetro seja não nulo e pertença a um tipo específico X (equivalente ao _ as Long do Spock), utilizamos o parâmetro de tipo reificado do MockK any<Long>() ou ofType(Long::class):
|
|
3.7. Argumento que Satisfaz um Predicado Customizado (match { ... })
Por fim, se quisermos verificar uma condição customizada sobre o parâmetro recebido usando uma lambda booleana (equivalente à closure { it == 1L } do Spock), usamos o matcher match { ... } (ou matchNullable { ... }) do MockK:
|
|
Resumo
Nesta aula, aprendemos cinco pontos fundamentais:
- Criamos stubs em especificações do Kotest usando
mockk<T>()(oumockk<T>(relaxed = true)quando queremos que chamadas não configuradas retornem valores padrão). - Isolamos nossos stubs entre os casos de teste usando
isolationMode = IsolationMode.InstancePerLeafouclearMocks(...)nobeforeEach. - Configuramos restrições de argumentos dentro do bloco
every { ... }usando literais,not(...),any(),anyNullable(),ofType(...)eallAny(). - Podemos implementar restrições de argumentos arbitrárias e expressivas utilizando predicados com
match { ... }.