// Testing · 04
Test Data Builders
A valid default object; each test changes only what it is about.
How it fits together
- Tax test uses / calls CustomerBuilder (1 WithCountry)
- CustomerBuilder creates Customer (2 Build())
Spot it
- Test sets twelve fields it does not care about
- Adding one required field breaks 80 tests
- Cannot tell what makes this test special
It costs
One more class to maintain, but a new required field needs one change, not sixty.
In code
public sealed record Customer(string Country, bool Active, int OpenInvoices);public sealed record CustomerBuilder(string Country = "DE"){ public CustomerBuilder WithCountry(string country) => this with { Country = country }; public Customer Build() => new(Country, Active: true, OpenInvoices: 1);}public sealed class TaxCalculatorTests{ [Fact] public void VatRate_CustomerInUs_IsZero() { // Arrange var customer = new CustomerBuilder() .WithCountry("US") .Build(); // Act var rate = new TaxCalculator().VatRateFor(customer); // Assert rate.ShouldBe(0m); }}Go deeperThe full explanation and what to ask for · 1 min readExpandCollapse
A test should show the one fact it depends on. When setup builds a full object each time, the reader has to compare the arrange block with the expected result to find which field matters. A builder starts from a valid default and exposes named changes, so the difference is the only thing on screen.
Keep the defaults valid under the domain rules, so a test that overrides nothing still exercises a legal state. Name builder methods after the domain (WithCountry, Overdue), not after the fields they set, and build the object only at the end. Defaults live in one place, so a change to what counts as a valid customer touches one file.
Watch for a builder that grows into a second copy of the domain model, with validation and branches of its own. When NOT to: a test whose point is the exact shape of an object, such as a serialisation contract, should write the whole object out so a shape change shows in the diff.
Ask for it
- builder
- valid default
- fluent override
- intent-revealing name
- object mother
- test data
Add a CustomerBuilder to the test project with valid defaults (active, EUR, country DE, one open invoice). Tests override only the field under test, such as WithCountry("US") for the tax case. Remove the 14-line object literals repeated across the 60 tests.