Neste post, vamos explorar algumas boas práticas na escrita de Test Data Builders. Mas para entender o que são “bons” builders, primeiro precisamos entender o que são builders “ruins”.
“Bom” e “ruim” são distinções claramente subjetivas, e os exemplos que listei abaixo são baseados na minha experiência pessoal e refletem a minha opinião apenas.
Erros comuns e Antipatterns
Builders ajudam bastante, mas temos que tomar alguns cuidados. Vamos ver alguns antipatterns comuns que já observei muitas pessoas fazendo (inclusive eu).
Tentar prever o futuro
Um erro comum que muitos desenvolvedores cometem ao implementar novo builder para testes é tentar prever o futuro. Eles fazem isso criando métodos para configurar cada propriedade do objeto em questão, mesmo quando no momento, só precisam customizar uma ou duas propriedades, como no exemplo abaixo: com um monte de código basicamente inútil, e que pode não ser nem útil quando essas propriedades vierem a ser customizadas.
Nessas horas é bom nos lembrarmos do princípio YAGNI, ou You aren’t gonna need it! (Você não vai precisar disso!), que tende a ser bastante verdadeiro. Desenvolvedores, como qualque outro ser humano, são péssimos em adivinhar o futuro. A recomendação aqui é simples: implemente customizações apenas para o que for necessário.
Inicializar os valores padrão direto nos fields
Outro erro muito fácil de cometer e ser tentado a jogar os valores padrão direto nos fields, como é feito na imagem abaixo.
public class EnderecoClienteBuilder
{
private string _logradouro = "Rua 7 de Setembro";
private int _numero = 123;
private string _bairro = "Centro";
private string _municipio = "Timbó";
private string _cep = "89120-000";
public EnderecoCliente Construir()
{
return new EnderecoCliente
{
Logradouro = _logradouro,
Numero = _numero,
Bairro = _bairro,
Municipio = _municipio,
CEP = _cep
};
}
// métodos de customização
}
Uma primeira desvantagem desta abordagem é que ela não prioriza explicitamente os valores customizados. Ao invés disso, dá maior importância aos valores padrão.
Vejamos agora o mesmo builder, só que com as inicializações no construtor:
public class EnderecoClienteBuilder
{
private string _logradouro;
private int? _numero;
private string _bairro;
private string _municipio;
private string _cep;
public EnderecoCliente Construir()
{
return new EnderecoCliente
{
Logradouro = _logradouro ?? "Rua 7 de Setembro",
Numero = _numero ?? 123,
Bairro = _bairro ?? "Centro",
Municipio = _municipio ?? "Timbó",
CEP = _cep ?? "89120-000"
};
}
// métodos de customização
}
À primeira vista pode parecer que estamos fazendo exatamente a mesma coisa: se não customizarmos uma propriedade, ela vai usar o valor padrão.
Mas deixar para tomar esta decisão na etapa de construção tem algumas vantagens. Em primeiro lugar, tudo o que envolve a definição de uma propriedade fica condensado num pequeno trecho de código como este:
Logradouro = _logradouro ?? "Rua 7 de Setembro"
Temos ali a propriedade e a seleção do valor final. Note que neste caso, o valor customizado vem primeiro. Estamos literalmente dizendo que será o valor customizado de _logradouro
, e caso contrário o valor padrão “Rua 7 de Setembro”.
Ou seja, ao selecionar o valor das propriedades nesta etapa, deixamos explícita a prioridade dos valores customizados sobre os valores-padrão. E explícito é quase sempre melhor que implícito.
Existe outra vantagem em implementar dessa forma: é mais fácil implementar cenários propositalmente inválidos. Isso pode soar estranho, mas vejamos um exemplo.
public class PedidoBuilder
{
private bool _criarSemOperacao = false;
private OperacaoPedido _operacaoPedido;
public Pedido Construir()
{
var operacao = _criarSemOperacao
? null
: _operacaoPedido ?? new OperacaoPedido(Operacoes.Consignacao, TiposMovimento.Consignacao);
return new Pedido(
new ChaveUnica("00450104201901042019131000000002620229"),
operacao: operacao
);
}
public PedidoBuilder SemOperacao()
{
this._criarSemOperacao = true;
return this;
}
public PedidoBuilder ComOperacao(OperacaoPedido operacao)
{
if (operacao == null)
SemOperacao();
this._operacao = operacao;
return this;
}
// resto do código de customização
}
Neste caso, a seleção do valor para a propriedade Operacao
do pedido deixa de ser binária com a introdução da possibilidade de ela não ter valor. Basicamente, estamos escolhendo ignorar o valor padrão.
var operacao = _criarSemOperacao
? null
: _operacaoPedido ?? new OperacaoPedido(Operacoes.Consignacao, TiposMovimento.Consignacao);
A lição aqui é que, sempre que possível, devemos adiar a seleção do valor de uma propriedade. Assim conseguimos manter toda essa lógica condensada, e o código vai tender a continuar simples.