Idiomatismos Avançados em Kotlin com MockK
Ao concluirmos esta aula, nós:
- Saberemos como configurar a resposta dinâmica de um método stubado utilizando expressões lambda concisas (
answers { ... }). - Seremos capazes de criar argument matchers customizados diretamente com expressões lambda (
match { ... }). - Saberemos como escrever asserções para o parâmetro real de um método invocado utilizando uma expressão lambda (
withArg { ... }) sem precisar instanciar umCapturingSlotmanualmente.
[!NOTE] Esta aula assume que:
Por Que Devemos Nos Importar com Código Boilerplate nos Testes?
Ferramentas de mocking tradicionais (como o Mockito clássico no Java pré-Java 8, que exigia classes anônimas new Answer<T>(), matchers customizados verbosos e ArgumentCaptor de três etapas) muitas vezes forçavam o desenvolvedor a escrever código repetitivo (boilerplate) que desviava a atenção da essência do caso de teste para detalhes burocráticos da ferramenta.
No Java 8+, surgiram lambdas para willAnswer(invocation -> ...) e bibliotecas auxiliares como mockito-java8 (argLambda e assertArg). Já no ecossistema Kotlin + MockK, esses três truques baseados em expressões lambda já vêm embutidos nativamente na biblioteca, sem precisar de nenhuma dependência extra!
Vamos dominar esses três idiomatismos que eliminam o código boilerplate dos nossos testes unitários.
1. Configurando Respostas Dinâmicas com Expressões Lambda (answers)
No Mockito antigo (Java 7), quando queríamos configurar uma resposta dinâmica que inspecionava os argumentos recebidos, éramos obrigados a instanciar uma classe interna anônima implementando a interface Answer<T>:
|
|
O problema desse código antigo é que tínhamos que escrever várias linhas de cerimônia que não tinham nada a ver com a regra do nosso caso de teste. Com expressões lambda (tanto em willAnswer(invocation -> ...) quanto no bloco nativo answers { ... } do MockK em Kotlin), eliminamos toda a classe anônima e escrevemos exclusivamente a lógica relevante para o teste:
|
|
O benefício dessa abordagem é imediato: escrevemos menos código e o código que escrevemos contém apenas a lógica relevante para o nosso caso de teste. Mas podemos ir ainda mais longe com os próximos dois truques!
2. Usando um Lambda Matcher (match { ... })
Suponha que queiramos configurar o parâmetro esperado de um método stubado (ou verificado) usando uma condição customizada que não é suportada pelos matchers básicos (eq(), any(), ofType()).
No Java clássico com Mockito, ou você escrevia uma classe inteira de ArgumentMatcher customizado, ou adicionava a biblioteca externa info.solidsoft.mockito:mockito-java8 para usar argLambda(task -> task.getId() == null) (ou argThat(task -> ...) nas versões mais recentes do Mockito).
No MockK, essa funcionalidade já faz parte do núcleo da biblioteca através da função match<T> { ... }!
Por exemplo, se quisermos garantir que o método save() da interface TaskRepository retorne o objeto Task salvo somente se o objeto Task passado como parâmetro não possuir id (task.id == null), configuramos o parâmetro esperado seguindo estes dois passos:
- Invocamos a função
match<Task> { ... }do pacoteio.mockk.matchcomo argumento derepository.save(...). - Especificamos a condição booleana dentro da expressão lambda:
{ task -> task.id == null }(ou simplesmente{ it.id == null }).
Depois de criarmos nosso argument matcher com uma expressão lambda, o código-fonte do nosso teste unitário fica assim:
|
|
Essa técnica com match { ... } nos permite expressar condições de correspondência arbitrárias e ricas sem escrever uma única linha de código boilerplate!
3. Capturando e Validando Argumentos com Lambda (withArg { ... })
Como vimos na aula sobre Mocks, quando queremos capturar o parâmetro real passado para um método e escrever asserções do AssertJ sobre ele usando um CapturingSlot (slot<T>() / ArgumentCaptor<T>), precisamos escrever três etapas separadas:
|
|
Como podemos notar, esse teste possui algumas linhas de cerimônia intermediária (val argument = slot<Long>(), capture(argument), val actualId = argument.captured).
Para eliminar completamente esse código boilerplate, o MockK oferece nativamente o matcher withArg<T> { ... } (o equivalente exato ao AssertionMatcher.assertArg(...) do mockito-java8 / Mockito.assertArg)!
Para capturar o parâmetro real e escrever asserções do AssertJ sobre ele em um único passo:
- Invocamos a função
withArg<Long> { ... }dentro da chamada verificada emverify. - Dentro da expressão lambda de
withArg, escrevemos diretamente nossas asserções do AssertJ garantindo que o objeto capturado contém as informações corretas!
Veja como o nosso teste unitário que verifica a chamada findById(1L) fica muito mais limpo usando withArg:
|
|
Como podemos ver, com withArg { ... } capturamos o parâmetro real passado ao método e executamos qualquer asserção do AssertJ (incluindo soft assertions ou asserções customizadas de domínio!) sem precisar declarar variáveis de slot ou ArgumentCaptor. O melhor dessa solução é que nosso teste unitário fica muito mais enxuto e destaca imediatamente a essência do caso de teste!
Resumo
Nesta aula, aprendemos três idiomatismos poderosos em Kotlin com o MockK:
- Podemos configurar respostas dinâmicas que inspecionam argumentos usando uma expressão lambda limpa com
answers { firstArg<T>() ... }(substituindo classes anônimasAnswer<T>). - Podemos criar argument matchers customizados inline utilizando expressões lambda com
match { ... }. - Podemos capturar o parâmetro real passado para um método e executar asserções do AssertJ diretamente dentro de
verify { mock.method(withArg { ... }) }, eliminando o código boilerplate deslot<T>()/ArgumentCaptor.