d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
10 KiB
10 KiB
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
"""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
// 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
// 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.