Dyego Maas - Blog

Generative AI Consultant and Software Architect

How to implement test data builders in C# with ForeverFactory

How to implement test data builders in C# with ForeverFactory

In this article, I introduce ForeverFactory, an open-source library I wrote to make creating objects for tests easier.

8 min read

In the previous articles in this series, we explored how test builders can simplify writing tests.

In this article, I introduce the ForeverFactory library, which aims to simplify implementing builders while maximizing configuration reuse.

Why did I create ForeverFactory?

I really like the approach of writing handcrafted test builders, fully tailored to a piece of software’s scenarios, as shown in this other article, but always writing them this way can get pretty repetitive.

One of the problems with this approach is that every time we want to customize how a property is set, we have to add a new method to our builder:

Traditional Handwritten Builder
[Fact]
public class PessoaBuilder
{
  private int? _idade;
  private string _nome;
  private string _cpf;
  // ...

  public Pessoa Construir()
  {
      return new Pessoa()
      {
          CPF = _cpf ?? "xxx.xxx.xxx-xx", // <1>
          Nome = _nome ?? "Ana", 
          Idade = _idade ?? 18
          // ...
      };
  }

  public PessoaBuilder ComCPF(string cpf)
  {
      _cpf = cpf;
      return this;
  }

  public PessoaBuilder ComIdade(int idade)
  {
      _idade = idade;
      return this;
  }

  public PessoaBuilder ComNome(string nome) // <2>
  {
      _nome = nome;
      return this;
  }

  // ...
}

public void Deve_fazer_algo()
{
  var pessoa = new PessoaBuilder()
      .ComNome("Arnoldo") // <3>
      .ComIdade(27)
      .Construir();
  
  // rest of the test
}
  1. We have a central place to configure a “default person”
  2. We had to hand-write a new ComNome(string nome) method to allow customizing each property
  3. From then on, we can customize the Nome property

And that’s the problem a very popular library sets out to solve: NBuilder.

NBuilder

NBuilder lets us skip writing those configuration methods over and over, thanks to a fluent, extensible interface.

For comparison, the previous example would look like this:

NBuilder
[Fact]
public void Deve_fazer_algo()
{
  var pessoa = Builder<Pessoa>().CreateNew()
      .With(x => x.Name = "Arnoldo")
      .With(x => x.Idade = 27)
      .Build();
  
  // rest of the test
}

As the test above shows, NBuilder makes it possible to flexibly configure any property of the objects we’re building, and that alone solves the problem of writing these customization methods by hand.

NBuilder also makes it easy to create collections of objects, as in the following example:

NBuilder - Collections
[Fact]
public void Deve_fazer_algo()
{
  var dezPessoasCentenarias = Builder<Pessoa>().CreateListOfSize(10)
      .With(x => x.Idade = 100)
      .Build();
  
  // rest of the test
}

But that flexibility comes at a cost: reusing common scenarios gets harder.

Imagine our test suite has 300 tests that need Pessoa (person) objects, all with valid CPFs (the Brazilian taxpayer ID), but with slightly different configurations.

Setup Duplication
[Fact]
public void Teste_1()
{
  var pessoaCentenaria = Builder<Pessoa>().CreateNew()
      .With(x => x.CPF = "xxx.xxx.xx-xx")
      .With(x => x.Idade = 100)
      .Build();
  
  // rest of the test
}

// ...

[Fact]
public void Teste_N()
{
  var menorDeIdade = Builder<Pessoa>().CreateNew()
      .With(x => x.CPF = "xxx.xxx.xx-xx")
      .With(x => x.Idade = 17)
      .Build();
  
  // rest of the test
}

As the example above shows, despite the flexibility, we end up running into duplicated test setup. That’s a problem I set out to solve with ForeverFactory, drawing inspiration from a library in the Python ecosystem called FactoryBoy.

FactoryBoy

The FactoryBoy library lets us create reusable factories, which can be customized further to fit each test scenario.

FactoryBoy
import factory

class Pessoa:
 def __init__(self, cpf, idade, nome):
      self.cpf = cpf
      self.idade = idade
      self.nome = nome


class PessoaFactory(factory.Factory):
  class Meta:
      model = Pessoa

  cpf = "xxx.xxx.xxx-xx" # <1>
  idade = 18
  nome = "Ana"


class PessoaTests(unittest.TestCase):

  def test_1(self):

      pessoa_centenaria = PessoaFactory(idade=100) # <2>
      
      # rest of the test
  #...
  def test_N(self):

      menor_idade = PessoaFactory(idade=17)
      
      # rest of the test
  1. The PessoaFactory class holds the whole default configuration of a person and sets important characteristics, for example, every person has a CPF
  2. Even though the test customizes only the age property, the remaining properties, such as the CPF, are inherited from the configuration defined in the PessoaFactory class

That way, each test stays focused on what matters most for that specific scenario, making it more concise without giving up on configuring each object correctly.

As the example above shows, just like with our handwritten builders, FactoryBoy lets us maximize configuration reuse by providing a central place to formalize those configurations.

ForeverFactory

I built the ForeverFactory library drawing inspiration from these two excellent tools, aiming to bring both of these traits together in a single library:

  1. NBuilder’s configuration flexibility
  2. FactoryBoy’s configuration reuse through a central factory

As an example, here is a simple test written with ForeverFactory:

ForeverFactory - Basic Example
[Fact]
public void Teste_sem_factory_customizada() // <1>
{
  var pessoaCentenaria = MagicFactory.For<Pessoa>()
      .With(x => x.CPF = "xxx.xxx.xx-xx")
      .With(x => x.Idade = 100)
      .Build();
  
  // rest of the test
}

[Fact]
public void Teste_usando_uma_factory_customizada() // <2>
{
  var pessoaCentenaria = new PessoaFactory()
      .With(x => x.Idade = 100)
      .Build();
  
  // rest of the test
}
  1. A test without a custom factory, as in NBuilder
  2. A test with a custom PessoaFactory factory, as in FactoryBoy

In the following sections, we’ll look in more detail at what ForeverFactory can do and how to use the library to write lean, concise tests.

Creating objects with ForeverFactory

As we saw in the example above, we can easily create objects in the same style as NBuilder:

ForeverFactory - Single Object
var pessoaCentenaria = MagicFactory.For<Pessoa>()
  .With(x => x.CPF = "xxx.xxx.xx-xx")
  .With(x => x.Idade = 100)
  .Build();

We can also create collections of objects using the Many(x) method:

ForeverFactory - Collections
var cemPessoas = MagicFactory.For<Pessoa>()
  .Many(100)
  .With(x => x.CPF = "xxx.xxx.xx-xx")
  .With(x => x.Idade = 100)
  .Build();

When creating collections of objects, we can also apply some extra configuration:

ForeverFactory - Specific Configurations
var cemPessoas = MagicFactory.For<Pessoa>()
  .Many(100)
  .WithFirst(50, x => x.Idade = 17) // <1>
  .WithLast(50, x => x.Idade = 18)  // <2>
  .Build();
  1. The first 50 people will be 17 years old
  2. The last 50 people will be 18 years old

Custom factories

We can create custom factories by extending the MagicFactory<T> class:

Custom Factory
public class PessoaFactory : MagicFactory<Pessoa>
{
  protected override void Customize(ICustomizeFactoryOptions<Pessoa> customization)
  {
      customization // <1>
          .Set(x => x.CPF = "xxx.xxx.xxx-xx") // <2>
          .Set(x => x.Nome = "Albert Einstein")
          .Set(x => x.Age = 56);
  }
}
  1. The customization object lets us configure how a “default Pessoa” should be built
  2. The Set method tells the factory to initialize the property with the given value. This value can be overridden later, case by case

Building complex sets of objects

Consider a custom factory like the PessoaFactory class below:

PessoaFactory.cs

PessoaFactory.cs
public class PessoaFactory : MagicFactory<Pessoa>
{
  protected override void Customize(ICustomizeFactoryOptions<Pessoa> customization)
  {
      customization
          .Set(x => x.CPF = "xxx.xxx.xxx-xx")
          .Set(x => x.Nome = "Albert Einstein")
          .Set(x => x.Age = 56);
  }
}

With it, we can build more complex sets for our test scenarios, as needed:

Teste.cs

Teste.cs - Complex Sets
var trintaPessoas = new PessoaFactory()
  .Many(10).With(x => x.Idade = 17)                   // <1>
  .Plus(10).With(x => x.Idade = 18)                   // <2>
  .Plus(9).With(x => x.Idade = 100)                   // <3>
  .PlusOne().With(x => x.Nome = "Stephen Hawking")    // <4>
  .Build();
  1. Creates 10 Pessoa instances with the configuration { CPF = "xxx.xxx.xxx-xx", Nome = "Albert Einstein", Idade = 17 }
  2. Creates 10 Pessoa instances with the configuration { CPF = "xxx.xxx.xxx-xx", Nome = "Albert Einstein", Idade = 18 }
  3. Creates 9 Pessoa instances with the configuration { CPF = "xxx.xxx.xxx-xx", Nome = "Albert Einstein", Idade = 100 }
  4. Creates 1 Pessoa instance with the configuration { CPF = "xxx.xxx.xxx-xx", Nome = "Stephen Hawking", Idade = 56 }

Benchmark

For comparison, I put together a few benchmark scenarios with equivalent configuration, comparing ForeverFactory 4.0.2 and NBuilder 6.1.0:

MethodMeanErrorStdDevGen 0Gen 1Allocated
BuildSingleObjectForeverFactory683.6 ns7.34 ns6.86 ns0.1373-1,152 B
BuildSingleObjectNBuilder1,939.7 ns20.81 ns19.47 ns0.0935-784 B
BuildThousandObjectsForeverFactory243,524.0 ns3,502.33 ns2,924.61 ns53.71095.8594449,403 B
BuildThousandObjectsNBuilder1,555,241.7 ns17,075.19 ns14,258.56 ns76.171915.6250653,402 B

These results came from the following benchmark tests:

Benchmark Tests
[Benchmark]
public void BuildSingleObjectForeverFactory()
{
  MagicFactory.For<Person>().With(x => x.Name = PersonName).Build();
}

[Benchmark]
public void BuildSingleObjectNBuilder()
{
  Builder<Person>.CreateNew().With(x => x.Name = PersonName).Build();
}

[Benchmark]
public void BuildThousandObjectsForeverFactory()
{
  MagicFactory.For<Person>().Many(1000).With(x => x.Name = PersonName).Build().ToList();
}

[Benchmark]
public void BuildThousandObjectsNBuilder()
{
  Builder<Person>.CreateListOfSize(1000).All().With(x => x.Name = PersonName).Build();
}

Conclusion

The ForeverFactory library lets us create test objects flexibly and reuse important configuration by implementing custom factories. On top of that, in equivalent scenarios, it’s up to 6x faster than NBuilder.

We can use it to write tests that focus on the details that matter for each scenario, and to extend valid base scenarios.