Enfatizando o Estado Relevante do Objeto Criado
Ao concluirmos esta aula, nós:
- Compreenderemos por que precisamos enfatizar o estado relevante do objeto criado em nossos testes.
- Seremos capazes de criar um Test Data Builder que destaca apenas o estado importante usando valores padrão intencionais (insane defaults).
- Seremos capazes de escrever métodos fábrica estáticos sobre o Test Data Builder que enfatizam o estado do objeto e eliminam o ruído visual.
- Você está familiarizado com o Sistema Sob Teste (SUT).
- Você sabe criar dados de teste com métodos fábrica locais e classes Object Mother.
- Você sabe criar e sobrescrever objetos com estado usando Test Data Builders.
No contexto desta aula, o estado de um objeto é definido pelo valor de um campo ou pela combinação de valores de múltiplos campos. Por exemplo, se um objeto
Task está aberto (OPEN):
- O campo
closerdeve sernull. - O valor do campo
statusdeve serTaskStatus.OPEN. - O campo
resolutiondeve sernull.
Excesso de Detalhes Nos Deixa Cegos
Vamos refrescar nossa memória e observar uma classe de teste que possui três métodos de teste:
- O primeiro método de teste cria uma tarefa aberta que não está atribuída a ninguém.
- O segundo método de teste cria uma tarefa aberta que está atribuída a uma pessoa específica.
- O terceiro método de teste cria uma tarefa que foi fechada como duplicada.
O código-fonte atual da nossa classe de teste é o seguinte:
|
|
Esses métodos de teste fazem sentido se estivermos testando uma funcionalidade de atualização completa que lê todos os campos da tarefa. Contudo, existem muitas situações em que nos importamos apenas com o estado do objeto criado.
Se estivermos escrevendo testes para um componente de negócio que toma decisões baseando-se unicamente no estado da Task (por exemplo, se está aberta ou fechada), não devemos criar nossos dados de teste com todas essas linhas! O código acima possui muito ruído visual (withId, withCreator, withModifier, withTitle, withDescription) que esconde a verdadeira essência do nosso caso de teste.
A seguir, vamos eliminar todo esse ruído passo a passo.
Utilizando Valores Padrão (Default Values)
Podemos nos livrar do ruído desnecessário utilizando uma destas duas abordagens:
Primeiro, se o objeto criado usa construção estilo JavaBeans (setters abertos sem validação no construtor), poderíamos simplesmente deixar de invocar os métodos desnecessários no nosso TaskBuilder:
|
|
Nossa classe de teste parece muito mais limpa, mas há um problema sério: os objetos Task criados ficam em um estado estranho e inválido, pois campos obrigatórios de domínio como creator, modifier e title ficam null! Por isso, apenas omitir os campos sem valores padrão não é uma boa solução.
Segundo, se o objeto criado utiliza o padrão Builder de produção (ou invariantes no bloco init {} do Kotlin), é muito provável que o builder de produção valide os campos obrigatórios para impedir a criação de objetos inválidos. Por exemplo, o builder de Task pode garantir que:
- Não podemos criar uma tarefa sem
creator(creator != null). - Não podemos criar uma tarefa sem
modifier(modifier != null). - Não podemos criar uma tarefa com
titlenulo ou vazio (!title.isNullOrEmpty()).
Se o nosso builder de produção fizer essas checagens de sanidade (sanity checks), o código-fonte da classe Task será semelhante a este:
|
|
Quando olhamos para essa classe Task, percebemos que o builder de produção lançará uma exceção se tentarmos chamar .build() sem configurar creator e title.
Felizmente, existe uma solução simples e elegante para esse problema:
Como não queremos invocar métodos irrelevantes em nossos testes, mas precisamos satisfazer os campos obrigatórios da classe Task, definimos valores padrão (default values) diretamente nos campos do nosso TaskBuilder de teste!
Devemos sempre usar valores padrão obviamente fictícios ("insanos") no nosso Test Data Builder, pois isso deixa explícito para qualquer pessoa (inclusive ao inspecionar variáveis no debugger ou logs de falha) que aquele caso de teste não se importa com os valores daqueles campos!
Podemos configurar os valores padrão dos campos obrigatórios fazendo duas pequenas alterações no TaskBuilder que criamos na aula anterior:
- Definir o valor padrão do campo
creatorIdcomo-1L(NOT_IN_USE). Como um ID de banco de dados nunca é negativo em produção, fica óbvio que o caso de teste não se importa com o criador da tarefa. - Definir o valor padrão do campo
titlecomo"NOT IMPORTANT". Esse texto deixa imediatamente claro que o título da tarefa é irrelevante para o teste em questão.
Depois de aplicarmos essas mudanças ao nosso TaskBuilder, seu código-fonte fica assim:
|
|
object Tasks (padrão Object Mother), sinta-se à vontade! Aqui os colocamos diretamente no companion object do TaskBuilder porque temos apenas três métodos fábrica. Se o número de métodos fábrica crescer, movê-los para um object Tasks dedicado mantém a classe TaskBuilder enxuta e organizada.
Agora podemos atualizar nossos métodos de teste em TaskTest para utilizar nossos novos métodos fábrica:
|
|
Veja como nossos métodos de teste ficaram extraordinariamente limpos! Cada criação ocupa uma única linha autoexplicativa que comunica imediatamente o estado do objeto e o único dado relevante para o teste.
Evidência Real de Execução no Terminal
Execução dos testes com factory methods fluentes no Test Data Builder no Gradle:
Initialized native services in: /home/wc/.gradle/native
Initialized jansi services in: /home/wc/.gradle/native
Gradle Daemon started in 1 s 106 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.defaults.TaskRelevantStateTest > Deve criar tarefa aberta não atribuída enfatizando apenas o status relevante PASSED
com.testwithspring.starter.testdata.defaults.TaskRelevantStateTest > Deve criar tarefa aberta atribuída especificando apenas o ID do responsável PASSED
com.testwithspring.starter.testdata.defaults.TaskRelevantStateTest > Deve criar tarefa aberta não atribuída via Factory Method fluente no Builder PASSED
com.testwithspring.starter.testdata.defaults.TaskRelevantStateTest > Deve criar tarefa aberta atribuída via Factory Method fluente no Builder PASSED
com.testwithspring.starter.testdata.defaults.TaskRelevantStateTest > Deve criar tarefa fechada como duplicada via Factory Method fluente no Builder PASSED
Task - Enfatizando o Estado Relevante do Objeto de Teste > 5 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
Quando Devemos Usar Esses Métodos Fábrica?
Recomendo usar um método fábrica sobre um Test Data Builder quando pudermos escrevê-lo com zero ou apenas um parâmetro. Devemos evitar métodos fábrica desse tipo com mais de um parâmetro por duas razões:
- Quando um método fábrica exige múltiplos parâmetros, seu propósito deixa de ser único e fica difícil dar um nome curto e cristalino que identifique a propriedade mais importante do objeto.
- O objetivo central dessa técnica é selecionar a propriedade mais importante para o estado testado e colocá-la em destaque absoluto. Se um cenário específico precisar customizar 3 ou 4 propriedades ao mesmo tempo, basta usar a API fluente do próprio
TaskBuilder()diretamente naquele teste!
Vamos resumir o que aprendemos nesta aula que encerra o tópico de Criação de Dados de Teste.
Resumo
Esta aula nos ensinou quatro pontos essenciais:
- Excesso de detalhes irrelevantes na criação de objetos gera ruído visual e esconde a real intenção do caso de teste.
- Devemos configurar valores padrão nos campos obrigatórios do nosso Test Data Builder para não precisarmos preenchê-los manualmente em todo teste.
- Devemos sempre escolher valores padrão “insanos” / autoexplicativos (como
-1Le"NOT IMPORTANT") para deixar claro que o teste não depende daqueles campos. - Podemos combinar métodos fábrica de zero ou um parâmetro com nosso Test Data Builder para enfatizar em uma única linha o estado exato do objeto criado.