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.