Dyego Maas - Blog

Generative AI Consultant and Software Architect

Best practices for implementing Test Data Builders in C#

Best practices for implementing Test Data Builders in C#

In this article we explore best practices and a few antipatterns that can plague the implementation of test data builders in C#. Check it out!

4 min read

In this post, we’ll explore some best practices for writing Test Data Builders. But to understand what “good” builders are, we first need to understand what “bad” builders are.

“Good” and “bad” are clearly subjective distinctions, and the examples I list below are based on my personal experience and reflect my opinion only.

Common mistakes and Antipatterns

Builders help a lot, but we need to be careful. Let’s look at a few common antipatterns I’ve seen many people fall into (myself included).

Trying to predict the future

A common mistake many developers make when implementing a new test builder is trying to predict the future. They do it by creating methods to configure every single property of the object at hand, even when, for now, they only need to customize one or two properties, as in the example below: a pile of basically useless code, which may not even turn out to be useful once those properties do need customizing.

Test builder full of junk

At times like this it’s good to remember the YAGNI principle, You aren’t gonna need it!, which tends to hold true. Developers, like any other human being, are terrible at guessing the future. The recommendation here is simple: implement customizations only for what you actually need.

Initializing default values right in the fields

Another mistake that’s very easy to make is being tempted to drop the default values straight into the fields, as done below.

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
      };
  }
  // customization methods
}

A first drawback of this approach is that it doesn’t explicitly prioritize the customized values. Instead, it gives more weight to the default values.

Now let’s look at the same builder, but with the initialization happening at build time:

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"
      };
  }
  // customization methods
}

At first glance it may look like we’re doing exactly the same thing: if we don’t customize a property, it uses the default value.

But leaving this decision to the build step has some advantages. First, everything involved in defining a property is condensed into a small snippet like this one:

Logradouro = _logradouro ?? "Rua 7 de Setembro"

There we have the property and the selection of its final value. Note that in this case, the customized value comes first. We’re literally saying it will be the customized value of _logradouro (street address), and otherwise the default value “Rua 7 de Setembro”.

In other words, by selecting property values at this step, we make the precedence of customized values over default values explicit. And explicit is almost always better than implicit.

There’s another advantage to implementing it this way: it’s easier to set up deliberately invalid scenarios. That may sound odd, so let’s look at an example.

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;
  }

  // rest of the customization code
}

In this case, selecting the value for the order’s (Pedido) Operacao property is no longer binary, since we’ve introduced the possibility of it having no value at all. Basically, we’re choosing to ignore the default value.

var operacao = _criarSemOperacao
  ? null
  : _operacaoPedido ?? new OperacaoPedido(Operacoes.Consignacao, TiposMovimento.Consignacao);

The lesson here is that, whenever possible, we should defer selecting a property’s value. That way we keep all that logic condensed, and the code will tend to stay simple.