# Test Data Builder ## Problem Tests that create complex domain objects are verbose and brittle. Each test repeats constructor calls with many parameters, most of which are irrelevant to the scenario. When the constructor signature changes, every test breaks. Shared fixture factories lose clarity about which fields matter for each test. ## Solution Create a **builder** class with sensible defaults for every field. Each field has a fluent setter (`with_*`) that returns the builder. A `build()` method produces the final object. Tests only specify the fields relevant to the scenario — everything else uses defaults. ## When to Use - Tests that construct objects with many fields (3+ constructor parameters). - Multiple tests need similar but slightly different object configurations. - Domain objects are immutable or use complex construction. ## When to Avoid - Objects are trivial (1-2 fields) — a direct constructor call is clearer. - A single factory method with keyword arguments suffices. - The builder would have more code than the tests it simplifies. ## Pseudocode ``` class UserBuilder: name = "default_user" email = "default@test.com" age = 25 role = "member" function with_name(n): name = n; return this function with_email(e): email = e; return this function with_age(a): age = a; return this function with_role(r): role = r; return this function build(): return User(name, email, age, role) // Test — only specify what matters test "admin can delete posts": admin = UserBuilder().with_role("admin").build() assert admin.can_delete_posts() ``` ## Python ```python """Test Data Builder pattern in Python.""" from dataclasses import dataclass from typing import Self # ── Domain model ────────────────────────────────────────────── @dataclass(frozen=True) class User: name: str email: str age: int role: str def can_delete_posts(self) -> bool: return self.role == "admin" def is_adult(self) -> bool: return self.age >= 18 def display(self) -> str: return f"{self.name} <{self.email}> ({self.role}, age {self.age})" # ── Builder ─────────────────────────────────────────────────── class UserBuilder: def __init__(self): self._name = "default_user" self._email = "default@test.com" self._age = 25 self._role = "member" def with_name(self, name: str) -> Self: self._name = name return self def with_email(self, email: str) -> Self: self._email = email return self def with_age(self, age: int) -> Self: self._age = age return self def with_role(self, role: str) -> Self: self._role = role return self def build(self) -> User: return User( name=self._name, email=self._email, age=self._age, role=self._role, ) # Convenience function def a_user() -> UserBuilder: return UserBuilder() # ── Tests ───────────────────────────────────────────────────── def test_admin_can_delete_posts(): admin = a_user().with_role("admin").build() assert admin.can_delete_posts() print(f"PASS: admin can delete posts -> {admin.display()}") def test_member_cannot_delete_posts(): member = a_user().with_name("bob").build() # role defaults to "member" assert not member.can_delete_posts() print(f"PASS: member cannot delete posts -> {member.display()}") def test_minor_is_not_adult(): minor = a_user().with_name("young_alice").with_age(16).build() assert not minor.is_adult() print(f"PASS: age {minor.age} is not adult -> {minor.display()}") def test_builder_creates_independent_objects(): builder = a_user().with_name("shared") user_a = builder.with_age(30).build() user_b = builder.with_age(40).build() # Note: builder is mutated, so user_b has age 40; user_a was built with age 30 assert user_a.age == 30 print(f"PASS: user_a age={user_a.age}, user_b age={user_b.age}") if __name__ == "__main__": test_admin_can_delete_posts() test_member_cannot_delete_posts() test_minor_is_not_adult() test_builder_creates_independent_objects() print("\nAll Test Data Builder tests passed.") ``` ## Go ```go // test_data_builder.go package main import "fmt" // ── Domain model ───────────────────────────────────────────── type User struct { Name string Email string Age int Role string } func (u User) CanDeletePosts() bool { return u.Role == "admin" } func (u User) IsAdult() bool { return u.Age >= 18 } func (u User) Display() string { return fmt.Sprintf("%s <%s> (%s, age %d)", u.Name, u.Email, u.Role, u.Age) } // ── Builder ────────────────────────────────────────────────── type UserBuilder struct { name string email string age int role string } func AUser() *UserBuilder { return &UserBuilder{ name: "default_user", email: "default@test.com", age: 25, role: "member", } } func (b *UserBuilder) WithName(name string) *UserBuilder { b.name = name return b } func (b *UserBuilder) WithEmail(email string) *UserBuilder { b.email = email return b } func (b *UserBuilder) WithAge(age int) *UserBuilder { b.age = age return b } func (b *UserBuilder) WithRole(role string) *UserBuilder { b.role = role return b } func (b *UserBuilder) Build() User { return User{ Name: b.name, Email: b.email, Age: b.age, Role: b.role, } } // ── Tests ──────────────────────────────────────────────────── func testAdminCanDeletePosts() { admin := AUser().WithRole("admin").Build() if !admin.CanDeletePosts() { panic("admin should be able to delete posts") } fmt.Printf("PASS: admin can delete posts -> %s\n", admin.Display()) } func testMemberCannotDeletePosts() { member := AUser().WithName("bob").Build() if member.CanDeletePosts() { panic("member should not be able to delete posts") } fmt.Printf("PASS: member cannot delete posts -> %s\n", member.Display()) } func testMinorIsNotAdult() { minor := AUser().WithName("young_alice").WithAge(16).Build() if minor.IsAdult() { panic("16-year-old should not be adult") } fmt.Printf("PASS: age %d is not adult -> %s\n", minor.Age, minor.Display()) } func testBuilderFluentChaining() { user := AUser(). WithName("carol"). WithEmail("carol@test.com"). WithAge(30). WithRole("admin"). Build() if user.Name != "carol" || user.Role != "admin" || user.Age != 30 { panic("fluent chaining produced wrong result") } fmt.Printf("PASS: fluent chaining -> %s\n", user.Display()) } func main() { testAdminCanDeletePosts() testMemberCannotDeletePosts() testMinorIsNotAdult() testBuilderFluentChaining() fmt.Println("\nAll Test Data Builder tests passed.") } ``` ## JavaScript ```javascript // test_data_builder.js — Fluent builder for test fixtures // ── Domain model ───────────────────────────────────────────── class User { constructor(name, email, age, role) { this.name = name; this.email = email; this.age = age; this.role = role; } canDeletePosts() { return this.role === "admin"; } isAdult() { return this.age >= 18; } display() { return `${this.name} <${this.email}> (${this.role}, age ${this.age})`; } } // ── Builder ────────────────────────────────────────────────── class UserBuilder { constructor() { this._name = "default_user"; this._email = "default@test.com"; this._age = 25; this._role = "member"; } withName(name) { this._name = name; return this; } withEmail(email) { this._email = email; return this; } withAge(age) { this._age = age; return this; } withRole(role) { this._role = role; return this; } build() { return new User(this._name, this._email, this._age, this._role); } } function aUser() { return new UserBuilder(); } // ── Tests ──────────────────────────────────────────────────── function testAdminCanDeletePosts() { const admin = aUser().withRole("admin").build(); console.assert(admin.canDeletePosts()); console.log(`PASS: admin can delete posts -> ${admin.display()}`); } function testMemberCannotDeletePosts() { const member = aUser().withName("bob").build(); console.assert(!member.canDeletePosts()); console.log(`PASS: member cannot delete posts -> ${member.display()}`); } function testMinorIsNotAdult() { const minor = aUser().withName("young_alice").withAge(16).build(); console.assert(!minor.isAdult()); console.log(`PASS: age ${minor.age} is not adult -> ${minor.display()}`); } function testFluentChaining() { const user = aUser() .withName("carol") .withEmail("carol@test.com") .withAge(30) .withRole("admin") .build(); console.assert(user.name === "carol" && user.role === "admin"); console.log(`PASS: fluent chaining -> ${user.display()}`); } testAdminCanDeletePosts(); testMemberCannotDeletePosts(); testMinorIsNotAdult(); testFluentChaining(); console.log("\nAll Test Data Builder tests passed."); ``` ## Related Patterns - **Builder** — the test data builder is an application of the builder pattern specialised for constructing test fixtures. - **Prototype** — clone a base fixture using prototype/copy to create variations without respecifying all defaults. - **Fluent Interface** — test data builders use a fluent interface to make fixture construction readable and expressive.