Valid-by-Default Builders for Test Data Link to heading

Most test data builders force you to fill in every property before you can create an object. That’s fine once or twice — but across dozens of tests, it’s a chore.

A better way: builders that produce valid objects by default, and only need extra input when you want to change something.

The Problem Link to heading

  • Too much setup noise in tests.
  • Copy-pasted constructor arguments that add no value.
  • Harder to read what a test is actually testing.

The Pattern Link to heading

Give your builder sensible defaults so this works:

var invoice = new InvoiceBuilder().Build();

And let targeted overrides be explicit:

var invoice = new InvoiceBuilder()
    .BuildInvoiceItems(i => i.WithSku("BONUS").WithQuantity(3))
    .Build();

Example:

public interface IBuild<T> { T Build(); }

public sealed class InvoiceBuilder : IBuild<Invoice>
{
    private Address _address = new Address("123 Main St", "AB1 2CD");
    private readonly List<Item> _items = { new Item("SKU-1", 1, 10) };

    public InvoiceBuilder BuildInvoiceItems(params Action<ItemBuilder>[] configs)
    {
        _items.Clear();
        foreach (var cfg in configs)
        {
            var b = new ItemBuilder();
            cfg(b);
            _items.Add(b.Build());
        }
        return this;
    }

    public Invoice Build() =>
        new Invoice(Guid.NewGuid().ToString(), DateTime.UtcNow, _address, _items);
}

Why It Works Link to heading

  • Fast defaults for common cases.
  • Explicit overrides for what matters in each test.
  • Keeps test code short and readable.

When It’s Too Much Link to heading

  • Avoid for trivial objects with 1–2 properties.
  • Don’t put domain logic in the builder — keep it dumb.

Takeaway Link to heading

Builders should be boring. Their beauty is in making your tests more expressive, not in being clever themselves.