Boas práticas para implementar Test Data Builders em C#

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.

Test builder com lixo

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.

Outras opções para compartilhar:
comments powered by Disqus