Simplificando setup de testes em C# com Test Data Builders
O setup dos seus testes é chato e repetitivo? Facilite sua vida usando Test Data Builders! Neste artigo mostramos como funcionam e simplificam seus testes.

Implementar uma boa suíte de testes unitários para nossos projetos pode ser muito recompensador, e na maioria dos casos, é indispensável. Mas, se não tomarmos certos cuidados, a manutenção desta suíte pode se tornar tediosa e até terrivelmente trabalhosa.
Vejamos um cenário muito comum, e que pode ser resolvido facilmente com o design pattern Builder aplicado a testes para obter o que chamamos de test data builders, ou simplesmente test builders. Ao longo deste artigo e de eventuais outros, vou chamar esses construtores de test builders.
Existem bibliotecas muito boas que podem nos ajudar a configurar esses builders, como o NBuilder, mas neste artigo vamos focar na escrita artesanal de builders especialmente desenhados para a aplicação.
#Testes repetitivos
Digamos que no nosso sistema imaginário, temos um objeto de negócio como este:
Uma classe real que representa um cliente provavelmente seria muito maior, mas esta será suficiente para o nosso exercício.
Agora imaginemos um cenário de teste simples que precisa passar um cliente para uma rotina.
À medida em que nossa aplicação vai crescendo, surgem mais e mais cenários que precisam instanciar um cliente para testar alguma regra de negócio. A configuração do cliente é muito parecido em todos eles, mas dependendo do dia, do desenvolvedor, e do alinhamento dos astros, as configurações começam a divergir, e talvez nem todos os cenários sejam realmente válidos.
Como podemos ver no exemplo a seguir, algumas semanas e muitos testes depois daquele primeiro cenário de teste que vimos um pouco acima, um desenvolvedor escreveu um novo teste para validar o CPF de um cliente.
Podemos ver que já existem divergências na forma como a propriedade CPF é configurada em cada cenário: algumas possuem CPFs formatados, outras não; alguns CPFs talvez sejam válidos, mas a maioria não é.
E então chega o dia em que o time decide que para resolver inconsistências nos tratamentos do CPF do cliente, vão implementar um objeto rico do tipo CPF, que vai se autovalidar.
A classe CPF
é mais ou menos assim:
O time está contente com essa implementação e decide que a propriedade CPF do cliente deverá ser alterada de string
para CPF
.
Mas então se dão conta de que o objeto cliente é instanciado em 652 testes, e de diferentes formas.
Alguém sugere que poderiam fazer esse ajuste facilmente com uma operação de substituição (CTRL+H), mas então se dá conta que a classe CPF é instanciada a partir de um long
, e não de uma String
, e que vão ter que ajustar na mão todos os 652 testes.
O problema que eles eles estão enfrentando é fruto do alto acoplamento entre os cenários de teste de o código de produção.
Este é o típico cenário que o uso de Test Builders poderia ter evitado uma grande dor de cabeça.
#Test Builders
Os Test Builders podem nos ajudar de várias formas:
- Evitando duplicação na configuração de testes
- Garantindo a consistência dos objetos construídos
- Melhorando legibilidade dos cenários de teste
A seguir, vamos investigar de que formas um Test Builder bem construído pode nos ajudar, e como eles afetam nossas suítes de testes.
#Evitando duplicação com Test Builders
Vamos começar imaginando como teria sido se o time fictício do cenário anterior tivesse usado um Test Builder desde o segundo ou terceiro cenário em que precisaram instanciar um cliente num cenário de teste.
Na sua forma mais simples, um ClienteBuilder
não passa de uma factory simples, que retorna uma instância padronizada de Cliente
.
Então todos os cenários estariam instanciando novos clientes assim:
Se agora o time tomar a decisão de transformar a propriedade CPF do cliente de String
para CPF
, será muito simples, pois basta fazer este ajuste no próprio builder, uma única vez:
E se algum teste passar a quebrar, provavelmente será porque o CPF informado não era válido em primeiro lugar.
Por fim, imaginemos que o time decidiu que a classe Cliente não deve mais ter setters públicos, e que todos os campos serão preenchidos via construtor:
Não fosse pelo uso de Test Builders (ou pelo menos algum tipo de factory), teríamos que ajustar todos os cenários de teste. Mas como temos nosso ClienteBuilder
, somente ele precisará ser alterado, e todos os cenários de testes passam ilesos pela alteração.
Abaixo, temos um ClienteBuilder totalmente customizável, usando este novo construtor:
E um teste que configura todos os campos de uma forma legível e fácil de acompanhar:
Como pudemos ver nos exemplos acima, o uso de test builders bem implementados ajuda a desacoplar os testes do código testado, ao mesmo tempo em que resulta em cenários de teste mais consistentes e fáceis de ler.
#Construindo objetos consistentes com Test Builders
Builders também podem ser usados para evitar a criação de objetos que sejam negocialmente inválidos, e isso acaba tendo um efeito colateral benéfico para nossos testes: eles ficam mais realistas.
Um exemplo corriqueiro é o de propriedades que precisam ser configuradas juntas para fazerem sentido.
Vamos para um exemplo fácil de visualizar: precisamos simular uma requisição HTTP para um sistema legado que retorna um objeto do tipo PrecoResponse
.
Vamos supor ainda que tenhamos duas regras para observar nessa integração:
Tipo
eDescricaoTipo
são sempre atualizados juntos. Existem três tipos de preço, dos quais os dois primeiros são tabelados;- Alguns preços fazem parte de uma tabela de preços aplicada por quantidade. Estes tipos, que obedece a uma faixa de quantidade de produtos vendidos. Quando
CodigoTabela
for zero,InicioFaixa
eFimFaixa
também serão.
Esta regra de negócio é um pouco complicada, e se nosso builder não for implementado com cuidado, essas regras não vão ficar claras nos testes, ou pior ainda, vários cenários podem não fazer sentido nenhum!
Podemos garantir que todos os cenários sejam construídos de acordo com estas regras simplesmente traduzindo-as para a interface do PrecoResponseBuilder
:
Como podemos ver no exemplo acima, será muito difícil criar estados inválidos usando esse PrecoResponseBuilder
. Cada configuração de preço atualiza em conjunto as propriedades Tipo
e DescricaoTipo
. Além disso, apenas os métodos para configurar preços dos tipos Praticado e Previsto permitem configurar tabelas de preço, conforme a regra de negócio.
Como pudemos ver, os builders podem ser extremamente benéficos para nossas suítes de testes e ajudam a proteger nossos testes de mudanças nos nossos sistemas, simplificando muito nosso dia-a-dia.