Criando Dados de Teste: Construtor Telescópico vs Argumentos Nomeados
Ao concluirmos esta aula, nós:
- Saberemos identificar as desvantagens do (anti)padrão do construtor telescópico (telescoping constructor (anti)pattern).
- Compreenderemos quando não devemos instanciar objetos diretamente por construtores posicionais (o equivalente à palavra-chave
newdo Java) em nossas classes de teste. - Saberemos quando devemos instanciar objetos diretamente pelo construtor em nossas classes de teste (e como os argumentos nomeados do Kotlin auxiliam nisso).
Invocar um construtor (usando a palavra-chave new em Java ou chamando NomeDaClasse(...) diretamente em Kotlin) é a maneira fundamental de instanciar novos objetos na JVM, e esta aula não critica a sua existência. O objetivo desta aula é demonstrar que devemos utilizá-lo no lugar certo e da maneira certa.
Esta aula pressupõe que:
Criando Novos Objetos Utilizando o (Anti)Padrão do Construtor Telescópico
Como vimos na aula anterior, a classe MailingAddress utiliza o (anti)padrão do construtor telescópico. A seguir, demonstraremos passo a passo como podemos criar novos objetos MailingAddress que representam endereços da Finlândia, da Suécia e dos Estados Unidos utilizando essas sobrecargas posicionais de construtor.
Contudo, antes de podermos criar novos objetos MailingAddress, precisamos criar uma nova classe de teste com JUnit Jupiter 6 e adicionar todas as constantes necessárias a ela.
Após criarmos nossa classe de teste inicial, seu código-fonte em Kotlin tem a seguinte aparência:
|
|
Agora estamos prontos para criar novos objetos MailingAddress. Vamos começar criando objetos MailingAddress que contêm endereços finlandeses.
Criando Endereços Finlandeses
Se quisermos criar um novo objeto MailingAddress que contenha um endereço finlandês (doméstico), precisamos especificar as seguintes informações:
- O nome do destinatário (receiver)
- Endereço de rua (street address) ou caixa postal (post office box)
- Código postal (post code)
- Cidade (city)
A seguir, criaremos dois endereços finlandeses:
Primeiro, forneceremos o endereço de rua ao construtor de 5 argumentos da classe MailingAddress.
Após criarmos esse novo endereço finlandês, o código-fonte da nossa classe de teste tem a seguinte aparência:
|
|
Este método de teste cria um endereço finlandês que tem a seguinte aparência:
|
|
Segundo, forneceremos uma caixa postal (post office box) ao construtor da classe MailingAddress.
Após adicionarmos a criação deste segundo endereço finlandês, o código-fonte da nossa classe de teste tem a seguinte aparência:
|
|
Este segundo método de teste cria um endereço finlandês que tem a seguinte aparência:
|
|
Ao analisarmos o uso posicional deste construtor, identificamos dois problemas claros:
- Ele recebe cinco objetos
String(ouString?) como argumentos do construtor. O problema é que, quando um construtor possui vários argumentos seguidos do mesmo tipo, torna-se difícil distinguir visualmente o papel de cada argumento sem consultar a assinatura do construtor. - O segundo ou o terceiro argumento do construtor deve ser sempre
null. Se quisermos criar um endereço que possui rua (streetAddress), o terceiro argumento deve sernull. Por outro lado, se quisermos criar um endereço que possui caixa postal (postOfficeBox), o segundo argumento deve sernull. Isso é confuso e essa regra posicional é difícil de memorizar.
Vamos avançar e criar novos objetos MailingAddress que contêm endereços suecos.
Criando Endereços Suecos
Se quisermos criar um novo objeto MailingAddress que contenha um endereço na Suécia, precisamos especificar as seguintes informações:
- O nome do destinatário (receiver)
- Endereço de rua (street address) ou caixa postal (post office box)
- Código postal (post code)
- Cidade (city)
- País (country)
A seguir, criaremos dois endereços suecos:
Primeiro, forneceremos o endereço de rua ao construtor de 6 argumentos da classe MailingAddress.
Após criarmos o novo endereço sueco com rua, o código-fonte da nossa classe de teste tem a seguinte aparência:
|
|
Este método de teste cria um endereço sueco que tem a seguinte aparência:
|
|
Segundo, forneceremos a caixa postal ao construtor da classe MailingAddress.
Após criarmos o segundo endereço sueco com caixa postal, o código-fonte da nossa classe de teste tem a seguinte aparência:
|
|
Este método de teste cria um endereço sueco que tem a seguinte aparência:
|
|
Este segundo construtor apresenta três problemas:
- Ele recebe seis objetos
Stringcomo argumentos do construtor. Se o objeto criado não contivesse informações tão conhecidas quanto um endereço (ou se os nomes das constantes não fossem explícitos), seria extremamente difícil distinguir entre os diferentes argumentos. - O segundo ou o terceiro argumento deve ser sempre
null. Este é exatamente o mesmo problema que notamos ao criar endereços finlandeses. - Podemos acabar utilizando este construtor para criar endereços finlandeses por engano. Se quisermos criar um endereço finlandês usando este construtor de 6 parâmetros, basta definir o valor do argumento
countrycomonull. Isso é um problema porque agora temos mais de uma maneira de fazer a mesma coisa e precisamos lembrar de escolher o construtor “correto”.
A seguir, criaremos novos objetos MailingAddress que contêm endereços dos Estados Unidos.
Criando Endereços dos Estados Unidos
Se quisermos criar um novo objeto MailingAddress que contenha um endereço nos Estados Unidos, precisamos especificar as seguintes informações:
- O nome do destinatário (receiver)
- Endereço de rua (street address) ou caixa postal (post office box)
- Cidade (city)
- Estado (state)
- Código postal (post code)
- País (country)
A seguir, criaremos dois endereços norte-americanos:
Primeiro, forneceremos o endereço de rua ao construtor de 7 argumentos da classe MailingAddress.
Após criarmos o novo endereço dos EUA com rua, o código-fonte da nossa classe de teste tem a seguinte aparência:
|
|
Este método de teste cria um endereço dos EUA que tem a seguinte aparência:
|
|
Segundo, forneceremos a caixa postal ao construtor da classe MailingAddress.
Após adicionarmos a criação do endereço dos EUA com caixa postal, o código-fonte da nossa classe de teste tem a seguinte aparência:
|
|
Este método de teste cria um endereço dos EUA que tem a seguinte aparência:
|
|
Este construtor de 7 parâmetros apresenta quatro problemas graves:
- Ele recebe sete objetos
Stringcomo argumentos do construtor. Em chamadas posicionais, isso é inaceitável para a legibilidade e segurança contra erros de digitação. - O segundo ou o terceiro argumento deve ser sempre
null. Infelizmente, usando apenas construtores posicionais, não há como evitar esse problema confuso. - Podemos utilizar este mesmo construtor para criar endereços finlandeses e suecos também. Se quisermos criar um endereço finlandês, basta definir os argumentos
stateecountrycomonull. Se quisermos criar um endereço sueco, basta definir o argumentostatecomonull. Temos mais opções agora, mas essa ambiguidade não é algo positivo. - A ordem dos argumentos do construtor só faz sentido se a pessoa desenvolvedora for da Finlândia (ou da Europa). Se um desenvolvedor dos Estados Unidos utilizasse este construtor posicional, provavelmente o detestaria, pois esperaria que os argumentos estivessem na ordem padrão americana:
receiver,streetAddress,postOfficeBox,city,state,postCodeecountry(ondecityestatevêm antes depostCode).
Como o Kotlin Mitiga Parte Desse Problema com Argumentos Nomeados e Valores Padrão
Em Kotlin 2.4, quando nós mesmos escrevemos a classe MailingAddress, não precisamos recorrer ao antipadrão de múltiplos construtores secundários em cascata. Podemos declarar um único construtor primário com valores padrão (= null) e invocá-lo utilizando argumentos nomeados (named arguments):
|
|
Com argumentos nomeados em Kotlin, a chamada no teste elimina a necessidade de passar null explicitamente e permite informar os parâmetros na ordem que fizer mais sentido para quem lê (inclusive a ordem americana city, state, postCode):
|
|
Ainda assim, mesmo em Kotlin, instanciar o construtor diretamente dentro de dezenas de métodos de teste exige repetir todos os campos obrigatórios (receiver, postCode, city, etc.) toda vez que precisamos de um endereço qualquer. Por isso, vamos descobrir agora quando não devemos instanciar construtores diretamente em nossas classes de teste (o equivalente a usar new diretamente no Java).
Quando Não Devemos Instanciar Construtores Diretamente nos Testes
Não devemos invocar construtores diretamente em nossas classes de teste (usando new em Java ou chamadas diretas de construtor em cada teste no Kotlin) se qualquer uma das seguintes condições for verdadeira:
Primeiro, o construtor possui “muitos” argumentos obrigatórios. Não especificaremos um número exato aqui, pois essa é uma decisão contextual que você deve tomar com sua equipe. Contudo, a partir do momento em que você começa a cometer erros ao preencher parâmetros ou sente que criar novos objetos nos testes está se tornando trabalhoso e repetitivo, seu construtor muito provavelmente exige argumentos demais para ser chamado diretamente em todo teste.
Segundo, a classe em questão utiliza o (anti)padrão do construtor telescópico ou possui múltiplos construtores sobrecarregados. Devemos incentivar as pessoas a escreverem testes que sejam fáceis de ler e de escrever. Se invocarmos os construtores diretamente nessa situação, todas as pessoas que lerem ou escreverem nosso código de teste precisarão conhecer os diferentes construtores E terão que lembrar de utilizar cada construtor apenas para um propósito específico. Isso gera confusão e não nos ajuda a atingir nosso objetivo de ter testes claros.
Terceiro, a ordem dos argumentos do construtor (quando chamado posicionalmente, como em classes Java consumidas pelo Kotlin ou chamadas sem argumentos nomeados) é lógica apenas para pessoas que pertencem a um grupo específico. Isso é fundamental se o código de teste puder ser lido ou escrito por pessoas que consideram aquela ordem de argumentos contraintuitiva. Se os nossos testes podem parecer confusos para parte da equipe, eles não são fáceis de ler nem de escrever.
A seguir, veremos quando devemos instanciar o construtor diretamente em nossas classes de teste.
Quando Devemos Instanciar Construtores Diretamente nos Testes
Devemos invocar o construtor diretamente em nossas classes de teste se qualquer uma das seguintes condições for verdadeira:
Primeiro, o construtor possui apenas poucos argumentos (por exemplo, objetos de valor simples como Assignee(userId = 99L) ou Creator(44L)). Se pudermos criar um novo objeto utilizando um construtor que recebe apenas um ou dois argumentos claros, não devemos sequer considerar opções mais complexas (como Factory Methods ou Test Data Builders), pois isso adicionaria complexidade desnecessária.
Segundo, os objetos criados são utilizados por apenas uma classe de teste ou por poucas classes de teste. Toda decisão de engenharia de software envolve trade-offs (compensações), e as outras opções de criação de dados de teste têm um custo de implementação e manutenção. Se estivermos nessa situação restrita, o custo de criar abstrações extras provavelmente será maior do que os benefícios. É por isso que, nesses casos, devemos criar nossos dados de teste invocando o construtor diretamente (preferencialmente com argumentos nomeados em Kotlin).
Agora sabemos identificar as desvantagens do (anti)padrão do construtor telescópico e sabemos avaliar quando devemos ou não instanciar construtores diretamente em nossas classes de teste. Vamos resumir o que aprendemos nesta aula.
Evidência Real de Execução no Terminal
Executando a suíte de testes de construtores telescópicos e argumentos nomeados no Gradle:
Initialized native services in: /home/wc/.gradle/native
Initialized jansi services in: /home/wc/.gradle/native
Gradle Daemon started in 1 s 110 ms
Starting Build
Settings evaluated using settings file '/home/wc/test-with-spring/code/01-starter-junit6/settings.gradle.kts'.
Projects loaded. Root project using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Included projects: [root project 'starter-junit6']
> Configure project :
Evaluating root project 'starter-junit6' using build file '/home/wc/test-with-spring/code/01-starter-junit6/build.gradle.kts'.
Using Kotlin Gradle Plugin 2.4.21
Using Java Toolchain: Temurin 21.0.6 (/home/wc/.sdkman/candidates/java/21.0.6-tem)
Spring Boot 4.1.2 plugin applied.
All projects evaluated.
Selected primary task 'test' from project :
Tasks to be executed: [task ':compileKotlin', task ':compileJava', task ':processResources', task ':classes', task ':compileTestKotlin', task ':compileTestJava', task ':processTestResources', task ':testClasses', task ':test']
Tasks that were skipped: [task ':compileJava', task ':compileTestJava']
> Task :compileKotlin
Compiling with Kotlin compiler version 2.4.21 (JVM target 21)
Compiling Kotlin files from [/home/wc/test-with-spring/code/01-starter-junit6/src/main/kotlin]
> Task :processResources NO-SOURCE
> Task :classes
> Task :compileTestKotlin
Compiling with Kotlin compiler version 2.4.21 (JVM target 21)
Compiling Kotlin files from [/home/wc/test-with-spring/code/01-starter-junit6/src/test/kotlin]
> Task :processTestResources NO-SOURCE
> Task :testClasses
> Task :test
Starting process 'Gradle Test Executor 1'. Working directory: /home/wc/test-with-spring/code/01-starter-junit6 Command: /home/wc/.sdkman/candidates/java/21.0.6-tem/bin/java -ea -cp ...
Successfully started process 'Gradle Test Executor 1'
com.testwithspring.starter.testdata.MailingAddressTest > Deve criar endereço finlandês com rua via construtor telescópico de 5 parâmetros PASSED
com.testwithspring.starter.testdata.MailingAddressTest > Deve criar endereço finlandês com caixa postal via construtor telescópico de 5 parâmetros PASSED
com.testwithspring.starter.testdata.MailingAddressTest > Deve criar endereço sueco com rua via construtor telescópico de 6 parâmetros PASSED
com.testwithspring.starter.testdata.MailingAddressTest > Deve criar endereço sueco com caixa postal via construtor telescópico de 6 parâmetros PASSED
com.testwithspring.starter.testdata.MailingAddressTest > Deve criar endereço dos EUA com rua via construtor completo de 7 parâmetros PASSED
com.testwithspring.starter.testdata.MailingAddressTest > Deve criar endereço dos EUA com caixa postal via construtor completo de 7 parâmetros PASSED
com.testwithspring.starter.testdata.MailingAddressTest > Deve criar endereço idiomático em Kotlin com argumentos nomeados e valores padrão PASSED
MailingAddress - Construtores Telescópicos vs Argumentos Nomeados > 7 tests completed, 0 failed, 0 skipped
Finished generating test XML results into: /home/wc/test-with-spring/code/01-starter-junit6/build/test-results/test
Finished generating test html results into: /home/wc/test-with-spring/code/01-starter-junit6/build/reports/tests/test
BUILD SUCCESSFUL in 2s
5 actionable tasks: 5 executed
Resumo
Esta aula nos ensinou três pontos fundamentais:
- O (anti)padrão do construtor telescópico torna o nosso código de teste difícil de ler e de escrever devido ao excesso de parâmetros do mesmo tipo, valores
nullposicionais e sobrecargas ambíguas (embora os argumentos nomeados do Kotlin ajudem a mitigar parte da confusão posicional). - Se a invocação direta do construtor (o
newdo Java) tornar nossos testes verbosos e difíceis de ler e escrever, não devemos utilizá-la diretamente espalhada pelas nossas classes de teste. - Devemos invocar o construtor diretamente em nossas classes de teste sempre que os seus prós superarem os seus contras (como em classes com poucos parâmetros ou usadas em pouquíssimos testes).