List Builder Pattern – Unit Testing Object Hierarchies in C#

When coding processing-intensive tasks, my classes are typically immutable and data-only (similar to F# records without structural equality). I then create classes that take the inputs through the constructor and transform the data with a single public method call.

In order to unit test this code, I need to be able to quickly generate dummy test data. At this point I usually turn to the builder pattern.

Sample Application

In the following examples we have a simple data hierarchy of a CAD assembly. An assembly has a name, ID, and a list of parts. Each part has a name, ID, mass, and quantity.

public class Assembly
{
	public Guid Id { get; private set; }
	public string Name { get; private set; }
	public IList<Part> Parts { get; private set; }

	public Assembly(Guid id, string name, IList<Part> parts)
	{
		Id = id;
		Name = name;
		Parts = parts;
	}
}

public class Part
{
	public Guid Id { get; private set; }
	public string Name { get; private set; }
	public int Quantity { get; private set; }
	public double Mass { get; private set; }

	public Part(Guid id, string name, int quantity, double mass)
	{
		Id = id;
		Name = name;
		Quantity = quantity;
		Mass = mass;
	}
}

After reading assembly data from a spreadsheet or some other user input, we need to validate it before inserting it into the database. We will unit test the following rules.

  • Assembly names must be unique.
  • Part names must be unique within and across assemblies.

Builder Pattern

The standard unit test builder pattern:

  1. Establishes default values for all fields
  2. Provides public setters to override the defaults – typically using a fluent style interface.
  3. Contains a Build() method (or an implicit conversion) which calls the constructor to build the type under test.

An assembly builder would look like this.

 
public class AssemblyBuilder
{
	private Guid _id = Guid.Parse("b5f6f602-f123-4730-b1b2-9c246f4807e8");
	private string _name = "Assembly 1";
	private IList<Part> _parts = new List<Part>();

	public AssemblyBuilder Id(Guid id)
	{
		_id = id;
		return this;
	}

	public AssemblyBuilder Name(string name)
	{
		_name = name;
		return this;
	}

	public AssemblyBuilder Parts(IList&lt;Part&gt; parts)
	{
		_parts = parts;
		return this;
	}

	public Assembly Build()
	{
		return new Assembly(_id, _name, _parts);
	}
}

A unit test for assembly name uniqueness would look like:

public void Validate_DuplicateAssemblyNames_Error()
{
	var assembly1 = new AssemblyBuilder()
					.Name("Wing").Build();

	var assembly2 = new AssemblyBuilder()
					.Name("Wing").Build();

	var validator = new AssemblyValidator(new[] { assembly1, assembly2 });
	var error = validator.Validate();

	Assert.Contains("duplicate names", error);
}

There are a couple issues with this approach already.

1. When making several assembly instances the default behavior is to create the same exact assembly. What’d we like is for each assembly be different but valid. For Guid fields all we need is a call to Guid.NewGuid(). For other fields we will need to make some helper methods.

2. It’s tedious to call multiple builders to make a list. We want something like NBuilder.

var assemblies = Builder<Assembly>CreateListOfSize(2)
					.TheFirst(2).With(a => a.Name = "Wing").Build();

However we can’t use NBuilder here because the class fields do not have public setters and there is no default constructor. NBuilder also nulls the Parts field by default.

3. We’ve completely avoided the parts collection in this implementation. This goes back to point #1 where we’d like to generate a list of unique and valid objects.

List Builder Pattern

Instead of the builder generating a single instance, we will design it to generate a list of objects. We will also use helper methods to generate random, valid test data – à la FsCheck.

Here is the PartsBuilder. The size of the list is passed in via the constructor. Given the size, a list of random data values is given for each field. The *Generator classes are simple helper classes to generate each field array. The Build() method simply loops over the field arrays and generates a list of parts.

public class PartsBuilder
{
	private int _size;
	private IList<Guid> _ids;
	private IList<string> _names;
	private IList<int> _quantities;
	private IList<double> _masses;
.
	public PartsBuilder(int size)
	{
		_size = size;
		_ids = new GuidsGenerator(size: size).Generate();
		_names = new OrderedStringGenerator(size, basename: "Part").Generate();
		_quantities = new RandomIntegerGenerator(size, min: 1, max: 999).Generate();
		_masses = new RandomDoubleGenerator(size, min: Double.Epsilon, max: Double.MaxValue).Generate();
	}

	public PartsBuilder Ids(params Guid[] ids)
	{
		_ids = ids;
		return this;
	}
	
	public PartsBuilder Names(params string[] names)
	{
		_names = names;
		return this;
	}
	
		
	public PartsBuilder Quantities(params int[] quantities)
	{
		_quantities = quantities;
		return this;
	}
	
	public PartsBuilder Masses(params double[] masses)
	{
		_masses = masses;
		return this;
	}
	
	public IList<Part> Build()
	{
		var parts = new List<Part>();
		for (int i = 0; i < _size; i++)
		{
			var part = new Part(_ids[i], _names[i], _quantities[i], _masses[i]);
			parts.Add(part);
		}

		return parts;
	}
}

The assembly builder follows the same pattern. Notice that we are now generating valid parts data. By default each assembly will have 1-10 valid parts assigned.

public class AssembliesBuilder
{
	private int _size;
	private IList<Guid> _ids;
	private IList<string> _names;
	private IList<PartsBuilder> _parts;

	public AssembliesBuilder(int size)
	{
		_size = size;
		_ids = new GuidsGenerator(size).Generate();
		_names = new OrderedStringGenerator(size, basename: "Assembly").Generate();
		_parts = Enumerable.Range(0, size).Select(i => new PartsBuilder(Gen.Int(1, 10))).ToList();
	}

	public AssembliesBuilder Id(IList<Guid> ids)
	{
		_ids = ids;
		return this;
	}

	public AssembliesBuilder Names(params string[] names)
	{
		_names = names;
		return this;
	}

	public AssembliesBuilder Parts(params PartsBuilder[] parts)
	{
		_parts = parts;
		return this;
	}

	public IList<Assembly> Build()
	{
		var assemblies = new List<Assembly>();
		for (int i = 0; i < _size; i++)
		{
			var assembly = new Assembly(_ids[i], _names[i], _parts[i].Build());
			assemblies.Add(assembly);
		}
		return assemblies;
	}
}

Our test code to detect duplicate assembly names is now similar to NBuilder. The main difference is that when overriding fields – each field in the list must be defined.

public void Validate_DuplicateAssemblyNames_Error()
{
	var assemblies = new AssembliesBuilder(size: 4)
		.Names("Wing", "Engine", "Wing", "Flaps")
		.Build();

	var validator = new AssemblyValidator(assemblies);
	var error = validator.Validate();

	Assert.Contains("duplicate names", error); // Wing
}

In our final example, we want to test duplicate part names across assemblies. Here we generate two assemblies and override the PartsBuilder for each one.

public void Validate_DuplicatePartNamesAcrossAssemblies_Error()
{
	var assemblies = new AssembliesBuilder(size: 2)
		.Parts(new PartsBuilder(size: 4)
				   .Names("Beam", "Bracket", "Widget", "Panel"),
			   new PartsBuilder(size: 2)
				   .Names("Widget", "Rivet")
		).Build();

	var validator = new AssemblyValidator(assemblies);
	var error = validator.Validate();

	Assert.Contains("duplicate names", error); // Widget
}

Leave a Reply