Testes Parametrizados com Kotlin e JUnit 6: Parte 2
Ao concluir esta aula do webinar sobre Kotlin 2.4.21, JUnit Jupiter 6.0.3, AssertJ 3.27.7 e Spring Boot 4.1.2 (com Gradle 9.8.2 Kotlin DSL e MockK 1.14), nós:
- Sabemos utilizar os mecanismos de conversão de argumentos fornecidos pelo JUnit 6.
- Sabemos como agregar os parâmetros de função das nossas funções de teste parametrizadas.
- Compreendemos como podemos utilizar testes parametrizados ao escrever testes para aplicações do mundo real.
Vamos começar convertendo nossos dados de teste em diferentes objetos.
Esta aula assume que:
- Você sabe escrever classes de teste com JUnit 6 e Kotlin (
01-escrevendo-classes-de-teste-com-kotlin-e-junit6)- Você sabe escrever testes parametrizados simples com Kotlin e JUnit 6 (
04-testes-parametrizados-com-kotlin-e-junit6-parte1)
Convertendo Nossos Dados de Teste em Diferentes Objetos
O JUnit 6 (JUnit Jupiter 6.0.3) possui um suporte muito versátil para conversão de argumentos. Para ser mais específico, ele suporta a conversão de alargamento de tipos primitivos (widening primitive conversion) e conta com diversos conversores de tipo implícitos embutidos (built-in implicit type converters) capazes de converter objetos String nos objetos que são passados para a nossa função de teste parametrizada. O conversor de tipo utilizado depende do tipo declarado em cada parâmetro da função.
Como o guia do usuário do JUnit Jupiter fornece uma excelente documentação sobre o processo de conversão implícita de tipos, não o descreverei em detalhes durante esta aula.
Leitura Adicional:
A seguir, descobriremos como podemos criar o parâmetro de função do nosso teste parametrizado utilizando uma função de fábrica (factory function) ou um construtor de fábrica (factory constructor).
Usando a Conversão de Fallback de String para Objeto
O JUnit 6 possui um mecanismo que fornece uma conversão automática de um objeto String para o tipo de destino solicitado (Fallback String-to-Object Conversion). Podemos utilizar esse mecanismo se o tipo de destino possuir exatamente uma função de fábrica adequada ou um construtor de fábrica adequado.
Vamos analisar dois exemplos que demonstram como podemos utilizar esse mecanismo.
Exemplo 1: Usando uma Função de Fábrica (Factory Function)
Se quisermos criar nosso parâmetro de função utilizando uma função de fábrica, o tipo de destino deve possuir uma função não privada e static (anotada com @JvmStatic no companion object em Kotlin) que receba um único objeto String como parâmetro de função e retorne uma instância do tipo de destino.
Por exemplo, se quisermos passar objetos Book para a nossa função de teste parametrizada, o código-fonte da classe Book deve ficar da seguinte forma:
|
|
Agora podemos escrever nossa função de teste parametrizada seguindo estes passos:
- Adicione uma nova função de teste parametrizada à nossa classe de teste e garanta que ela receba um objeto
Bookcomo parâmetro de função. - Especifique os nossos dados de teste.
- Garanta que o objeto
Bookrecebido como parâmetro de função possua o título correto.
Após escrevermos nosso teste parametrizado, o código-fonte da nossa classe de teste ficará da seguinte forma:
|
|
Exemplo 2: Usando um Construtor de Fábrica (Factory Constructor)
Se quisermos criar nosso parâmetro de função utilizando um construtor de fábrica, o tipo de destino deve possuir um construtor não privado que receba um único objeto String como argumento do construtor.
Por exemplo, se quisermos passar objetos Movie para a nossa função de teste parametrizada, o código-fonte da classe Movie deve ficar da seguinte forma:
|
|
Agora podemos escrever nossa função de teste parametrizada seguindo estes passos:
- Adicione uma nova função de teste parametrizada à nossa classe de teste e garanta que ela receba um objeto
Moviecomo parâmetro de função. - Especifique os nossos dados de teste.
- Garanta que o objeto
Movierecebido como parâmetro de função possua o título correto.
Após escrevermos nosso teste parametrizado, o código-fonte da nossa classe de teste ficará da seguinte forma:
|
|
Há dois pontos importantes que quero destacar:
- Se o tipo de destino possuir múltiplas funções de fábrica, essas funções de fábrica serão ignoradas pelo JUnit 6.
- Se o tipo de destino possuir tanto uma função de fábrica quanto um construtor de fábrica, o JUnit 6 utilizará a função de fábrica em vez do construtor.
Leitura Adicional:
Vamos avançar e descobrir como podemos utilizar um ArgumentConverter customizado.
Usando um ArgumentConverter Customizado
Se não pudermos utilizar os mecanismos de conversão de argumentos embutidos fornecidos pelo JUnit 6 e precisarmos converter um único objeto String em outro objeto, sempre podemos implementar um ArgumentConverter customizado e criar o parâmetro de função da nossa função de teste parametrizada utilizando o nosso novo ArgumentConverter.
Vamos supor que queremos passar objetos Album para a nossa função de teste parametrizada. O código-fonte da classe Album é o seguinte:
|
|
Se quisermos utilizar um ArgumentConverter customizado, devemos seguir estes passos:
Primeiro, devemos implementar um ArgumentConverter customizado seguindo estes passos:
- Crie uma nova classe que estenda a classe
SimpleArgumentConverter. - Sobrescreva a função
convert()da classeSimpleArgumentConverter. - Implemente a função
convert()retornando um novo objetoAlbum. Além disso, se o tipo de destino não forAlbum, lance uma novaIllegalArgumentException.
Após escrevermos nosso conversor de argumentos customizado, seu código-fonte ficará da seguinte forma:
|
|
Segundo, devemos escrever nossa função de teste parametrizada seguindo estes passos:
- Adicione uma nova função de teste parametrizada à nossa classe de teste e garanta que ela receba um objeto
Albumcomo parâmetro de função. - Garanta que o objeto
Albumseja criado pela classeAlbumArgumentConverter. - Especifique os nossos dados de teste.
- Garanta que o objeto
Albumrecebido como parâmetro de função possua o título correto.
Após escrevermos nosso teste parametrizado, o código-fonte da nossa classe de teste ficará da seguinte forma:
|
|
Leitura Adicional:
Agora já estamos familiarizados com os diferentes mecanismos de conversão de argumentos fornecidos pelo JUnit 6. A seguir, aprenderemos a agregar os parâmetros de função das nossas funções de teste parametrizadas.
Agregando Múltiplos Argumentos em um Único Objeto
Se utilizarmos uma fonte de argumentos que suporte múltiplos argumentos, o comportamento padrão do JUnit 6 assume que cada argumento fornecido à nossa função de teste parametrizada corresponde a um parâmetro de função individual. Em outras palavras, se fornecermos um grande número de argumentos para a nossa função de teste, a sua assinatura também se tornará extensa.
Podemos resolver esse problema utilizando a classe ArgumentsAccessor ou escrevendo um ArgumentsAggregator customizado. Vamos avançar e descobrir como utilizar essas técnicas.
Exemplo 1: Usando a Classe ArgumentsAccessor
Se quisermos utilizar a classe ArgumentsAccessor, devemos escrever nosso teste parametrizado seguindo estes passos:
- Adicione uma nova função de teste parametrizada à nossa classe de teste e garanta que ela receba um objeto
ArgumentsAccessorcomo parâmetro de função. - Configure os dados de teste.
- Crie um novo objeto
Personutilizando o objetoArgumentsAccessorrecebido como parâmetro de função. - Garanta que o objeto
Personcriado possua oide onamecorretos.
Após escrevermos nosso teste parametrizado, o código-fonte da nossa classe de teste ficará da seguinte forma:
|
|
A classe Person é uma data class simples que contém o id e o name de uma pessoa. Seu código-fonte é o seguinte:
|
|
Há três pontos que quero destacar:
- A classe
ArgumentsAccessorutiliza um índice baseado em zero (zero-based index).- A classe
ArgumentsAccessorsuporta os mecanismos embutidos de conversão de argumentos fornecidos pelo JUnit 6.- A classe
ArgumentsAccessoré uma boa escolha se quisermos simplesmente reduzir o número de parâmetros da função e não precisarmos converter nossos dados de teste em um objeto específico de forma reutilizável (como um objetoPersonem vários testes).Leitura Adicional:
Exemplo 2: Usando um ArgumentsAggregator Customizado
Se quisermos fornecer objetos Person para a nossa função de teste parametrizada utilizando um ArgumentsAggregator customizado, devemos seguir estes passos:
Primeiro, devemos criar uma classe ArgumentsAggregator customizada seguindo estes passos:
- Crie uma classe que implemente a interface
ArgumentsAggregator. - Sobrescreva a função
aggregateArguments(). - Implemente a função
aggregateArguments()retornando um novo objetoPerson. Além disso, se o número de argumentos não for igual a dois, lance uma novaIllegalArgumentException.
Após escrevermos nossa classe ArgumentsAggregator customizada, seu código-fonte ficará da seguinte forma:
|
|
Segundo, devemos escrever nossa função de teste parametrizada seguindo estes passos:
- Adicione uma nova função de teste parametrizada à nossa classe de teste e garanta que ela receba um objeto
Personcomo parâmetro de função. - Garanta que o objeto
Personseja criado pela classePersonAggregator. - Especifique os nossos dados de teste.
- Garanta que o objeto
Personrecebido como parâmetro do método possua oide onamecorretos.
Após escrevermos nossa função de teste parametrizada, seu código-fonte ficará da seguinte forma:
|
|
Leitura Adicional:
Agora já sabemos como agregar os parâmetros de função dos nossos testes parametrizados. Vamos avançar e descobrir como podemos tirar proveito dos testes parametrizados ao escrever testes para aplicações do mundo real.
Testes Parametrizados — Além do Básico
Não tenho visto muitos testes parametrizados no dia a dia de trabalho e, infelizmente, preciso confessar que também não costumava escrevê-los com tanta frequência. Isso é uma pena, pois o teste parametrizado é uma ferramenta extremamente útil quando estamos escrevendo testes para métodos de busca (finder methods).
Por exemplo, vamos supor que precisamos escrever testes para a classe PersonRepository. Essa classe possui um método chamado findById() que retorna as informações da pessoa encontrada. Se nenhuma pessoa for encontrada com o id fornecido, esse método lança uma NotFoundException.
A seguir, aprenderemos a tirar proveito dos testes parametrizados ao escrever testes para a classe PersonRepository. Quando quisermos escrever testes para o método findById(), devemos seguir estes passos:
Primeiro, devemos adicionar uma nova inner class à nossa classe de teste. Essa classe conterá os métodos de teste que garantem que o método findById() esteja funcionando conforme o esperado.
Após adicionarmos essa inner class à nossa classe de teste, o código-fonte da nossa classe de teste ficará da seguinte forma:
|
|
Segundo, devemos escrever um método de teste que garanta que o método findById() lance uma NotFoundException quando a pessoa solicitada não for encontrada.
Após escrevermos esse método de teste, o código-fonte da nossa classe de teste ficará da seguinte forma:
|
|
Terceiro, devemos escrever um teste parametrizado que garanta que o método findById() retorne as informações da pessoa encontrada.
Após escrevermos esse teste parametrizado, o código-fonte da nossa classe de teste ficará da seguinte forma:
|
|
Como podemos ver, ao combinarmos testes parametrizados com métodos de teste convencionais, torna-se bastante simples escrever testes limpos e abrangentes para métodos de busca. Essa abordagem é especialmente útil se o sistema sob teste utilizar uma lógica de mapeamento complexa para criar o objeto retornado e quisermos escrever testes para essa lógica.
Naturalmente, se utilizarmos testes parametrizados para essa finalidade, é uma excelente ideia utilizar soft assertions (asserções suaves do AssertJ), pois elas nos permitem coletar todos os erros de asserção e exibi-los de uma só vez quando um caso de teste falha.
Além disso, embora este exemplo invoque um repositório em memória codificado diretamente (hard-coded), podemos aplicar exatamente essa mesma técnica ao escrever testes de integração com o Spring Test Framework no Spring Boot 4.1.2.
Vamos resumir o que aprendemos nesta aula.
Resumo
Esta aula nos ensinou quatro coisas fundamentais:
- O JUnit 6 possui um mecanismo que fornece uma conversão automática de um objeto
Stringpara o tipo de destino solicitado caso o tipo de destino possua exatamente uma função de fábrica adequada ou um construtor de fábrica adequado. - Se não pudermos utilizar os mecanismos de conversão de argumentos embutidos fornecidos pelo JUnit 6 e precisarmos converter um único objeto
Stringem outro objeto, sempre podemos implementar umArgumentConvertercustomizado. - Se quisermos combinar múltiplos argumentos em um único parâmetro de função, devemos utilizar a classe
ArgumentsAccessorou implementar umArgumentsAggregatorcustomizado. - Se combinarmos testes parametrizados com métodos de teste normais, torna-se muito simples escrever testes limpos e abrangentes para métodos de busca (finder methods).