Test Design & Fixtures — Practice Tasks¶
Category: Craftsmanship Disciplines — design tests that read clearly, run fast, and manage their own data, so a failing test names a single broken behavior.
10 graded hands-on tasks with full Go, Java, and Python solutions. Try each before expanding the solution.
Table of Contents¶
- Task 1: Refactor a Tangled Test to AAA
- Task 2: Split a Multi-Concept Test
- Task 3: Build a Test Data Builder
- Task 4: Write an Object Mother
- Task 5: Eliminate a Mystery Guest
- Task 6: Make a Flaky Time Test Deterministic
- Task 7: Add Teardown That Runs on Failure
- Task 8: Pick the Right Test Double
- Task 9: Parameterize a Copy-Pasted Test
- Task 10: Break a Test Interdependence
- Practice Tips
Task 1: Refactor a Tangled Test to AAA¶
Goal: Rewrite a one-blob test into clear Arrange-Act-Assert, separated by blank lines, with one Act.
Given (Python):
def test_account():
a = Account(100); a.withdraw(30); assert a.balance == 70; a.deposit(10); assert a.balance == 80
Solution
### Pythondef test_withdraw_reduces_balance():
account = Account(balance=100) # Arrange
account.withdraw(30) # Act
assert account.balance == 70 # Assert
def test_deposit_increases_balance():
account = Account(balance=100) # Arrange
account.deposit(10) # Act
assert account.balance == 110 # Assert
@Test void withdraw_reduces_balance() {
Account account = new Account(100); // Arrange
account.withdraw(30); // Act
assertEquals(70, account.balance()); // Assert
}
@Test void deposit_increases_balance() {
Account account = new Account(100);
account.deposit(10);
assertEquals(110, account.balance());
}
Task 2: Split a Multi-Concept Test¶
Goal: A test asserts about three unrelated behaviors. Split it so each failure names one concept.
Given (Python):
def test_register():
user = register("ada@x.com")
assert user.active # user state
assert mailer.last_sent == "ada@x.com" # welcome email
assert audit.last == "REGISTER" # auditing
Solution
### Python **Why:** Three concepts (user state, email, audit) become three tests. Now a broken welcome email reports as `test_register_sends_welcome_email`, not a generic failure. Note: asserting several fields of *one* concept (the created user) in a single test is fine — only *separate behaviors* get split.Task 3: Build a Test Data Builder¶
Goal: Replace noisy 8-argument constructor calls in tests with a fluent builder that has valid defaults; each test overrides only the field it cares about.
Given (Java) — every test repeats the full constructor:
Solution
### Javaclass CustomerBuilder {
private String name = "Test";
private String email = "test@x.com";
private Tier tier = Tier.STANDARD;
private boolean verified = true;
private int balance = 0;
static CustomerBuilder aCustomer() { return new CustomerBuilder(); }
CustomerBuilder gold() { this.tier = Tier.GOLD; return this; }
CustomerBuilder unverified() { this.verified = false; return this; }
CustomerBuilder balance(int b) { this.balance = b; return this; }
Customer build() { return new Customer(name, email, tier, verified, balance, "US", LocalDate.EPOCH); }
}
// Tests now show ONLY what matters:
Customer vip = aCustomer().gold().build();
Customer suspicious = aCustomer().unverified().balance(-50).build();
from dataclasses import dataclass, replace
@dataclass
class Customer:
name: str = "Test"
email: str = "test@x.com"
tier: str = "STANDARD"
verified: bool = True
balance: int = 0
A_CUSTOMER = Customer()
vip = replace(A_CUSTOMER, tier="GOLD")
suspicious = replace(A_CUSTOMER, verified=False, balance=-50)
type CustomerBuilder struct{ c Customer }
func ACustomer() *CustomerBuilder { return &CustomerBuilder{Customer{Tier: "STANDARD", Verified: true}} }
func (b *CustomerBuilder) Gold() *CustomerBuilder { b.c.Tier = "GOLD"; return b }
func (b *CustomerBuilder) Unverified() *CustomerBuilder { b.c.Verified = false; return b }
func (b *CustomerBuilder) Balance(n int) *CustomerBuilder { b.c.Balance = n; return b }
func (b *CustomerBuilder) Build() Customer { return b.c }
// vip := ACustomer().Gold().Build()
Task 4: Write an Object Mother¶
Goal: For a small set of fixed, well-known scenarios, provide named canned instances that read like English.
Given: tests repeatedly need "a verified VIP," "an unverified newcomer," "a banned user."
Solution
### Pythonclass Customers:
@staticmethod
def vip(): return Customer(tier="GOLD", verified=True)
@staticmethod
def newcomer(): return Customer(tier="NONE", verified=False)
@staticmethod
def banned():
c = Customers.vip(); c.banned = True; return c
# Usage reads like a sentence:
order = Order(customer=Customers.vip())
Task 5: Eliminate a Mystery Guest¶
Goal: A test relies on data from a shared seed file. Make it build and show its own data.
Given (Python) — buggy: where did customer 42 come from?
def test_premium_discount():
order = place_order(customer_id=42, total=100) # 42 lives in seed.sql
assert order.discount == 10 # why 10? invisible
Solution
### Pythondef test_premium_discount(db):
customer = a_customer().premium().build() # build it here, visibly
db.save(customer)
order = place_order(customer_id=customer.id, total=100)
assert order.discount == 10 # premium → 10% is now obvious from the fixture
Task 6: Make a Flaky Time Test Deterministic¶
Goal: A test depends on the wall clock and can straddle a date boundary. Inject a clock so it's Repeatable.
Given (Python) — flaky:
def test_token_expired():
token = Token(created=datetime.now() - timedelta(hours=2))
assert token.is_expired() # depends on now(); fragile near boundaries
Solution
### Pythonclass Token:
def __init__(self, created): self.created = created
def is_expired(self, now): # time injected
return (now - self.created) > timedelta(hours=1)
def test_token_expired():
created = datetime(2025, 1, 1, 10, 0)
now = datetime(2025, 1, 1, 12, 30) # fixed
assert Token(created).is_expired(now)
class Token {
private final Instant created;
Token(Instant c) { this.created = c; }
boolean isExpired(Clock clock) {
return Duration.between(created, clock.instant()).toHours() >= 1;
}
}
@Test void token_expired() {
Clock fixed = Clock.fixed(Instant.parse("2025-01-01T12:30:00Z"), ZoneOffset.UTC);
Token token = new Token(Instant.parse("2025-01-01T10:00:00Z"));
assertTrue(token.isExpired(fixed));
}
type Token struct{ Created time.Time }
func (t Token) Expired(now time.Time) bool { return now.Sub(t.Created) > time.Hour }
func TestTokenExpired(t *testing.T) {
created := time.Date(2025, 1, 1, 10, 0, 0, 0, time.UTC)
now := time.Date(2025, 1, 1, 12, 30, 0, 0, time.UTC)
if !(Token{created}).Expired(now) { t.Fatal("want expired") }
}
Task 7: Add Teardown That Runs on Failure¶
Goal: A test opens a resource and closes it only on the happy path. Move cleanup into a hook so it runs even when an assertion fails.
Given (Python) — buggy: leaks on failure:
def test_export():
f = open("out.csv", "w")
write_report(f)
assert f.tell() > 0 # if this fails ↓
f.close() # skipped → leaked file handle
Solution
### Python@pytest.fixture
def out_file(tmp_path):
f = open(tmp_path / "out.csv", "w")
yield f
f.close() # teardown — runs on pass AND failure
def test_export(out_file):
write_report(out_file)
assert out_file.tell() > 0
class ExportTest {
Path tmp; Writer w;
@BeforeEach void setUp() throws IOException {
tmp = Files.createTempFile("out", ".csv");
w = Files.newBufferedWriter(tmp);
}
@AfterEach void tearDown() throws IOException { w.close(); Files.deleteIfExists(tmp); }
@Test void export_writes_data() throws IOException {
writeReport(w); w.flush();
assertTrue(Files.size(tmp) > 0);
}
}
Task 8: Pick the Right Test Double¶
Goal: For two specs, choose between a stub (state verification) and a mock (behavior verification), and justify the choice.
Spec A: "late_fee(due, clock) returns 110 when overdue." Spec B: "notify(user, mailer) sends exactly one email."
Solution
### Spec A — the clock is just an input → STUB + assert on the result ### Spec B — sending the email IS the behavior → MOCK + assert on the interaction **Why:** Use a **stub** when the double merely feeds data and you assert on the result — this survives refactoring. Use a **mock** only when the *interaction itself* is the requirement (an email must be sent). Mocking Spec A's clock and verifying `clock.now()` was called would pin implementation for no benefit.Task 9: Parameterize a Copy-Pasted Test¶
Goal: Five near-identical tests differ only in input/expected. Collapse them into one parameterized test that reports each case independently.
Given (Python):
def test_tier_free(): assert tier(0) == "free"
def test_tier_standard(): assert tier(50) == "standard"
def test_tier_premium(): assert tier(500) == "premium"
def test_tier_negative(): assert tier(-1) == "invalid"
Solution
### Python@pytest.mark.parametrize("amount, expected", [
(0, "free"),
(50, "standard"),
(500, "premium"),
(-1, "invalid"),
])
def test_tier_for_amount(amount, expected):
assert tier(amount) == expected
@ParameterizedTest
@CsvSource({ "0, free", "50, standard", "500, premium", "-1, invalid" })
void tier_for_amount(int amount, String expected) {
assertEquals(expected, Tier.of(amount));
}
func TestTier(t *testing.T) {
for name, tc := range map[string]struct{ amount int; want string }{
"free": {0, "free"}, "standard": {50, "standard"},
"premium": {500, "premium"}, "invalid": {-1, "invalid"},
} {
t.Run(name, func(t *testing.T) {
if got := Tier(tc.amount); got != tc.want {
t.Errorf("Tier(%d) = %q, want %q", tc.amount, got, tc.want)
}
})
}
}
Task 10: Break a Test Interdependence¶
Goal: Two tests share a mutable fixture; the second passes only if the first ran. Make each independent.
Given (Python) — buggy: order-dependent:
@pytest.fixture(scope="module")
def cart():
return Cart() # ONE cart shared by all tests
def test_add_item(cart):
cart.add("book")
assert len(cart.items) == 1
def test_empty_cart(cart):
assert len(cart.items) == 0 # FAILS if test_add_item ran first
Solution
### Python@pytest.fixture # default scope="function" → fresh per test
def cart():
return Cart()
def test_add_item(cart):
cart.add("book")
assert len(cart.items) == 1
def test_empty_cart(cart):
assert len(cart.items) == 0 # always passes — its own clean cart
Practice Tips¶
- Mark Arrange-Act-Assert until it's automatic; one Act per test.
- Split by concept — one reason to fail per test.
- Build objects with a builder/factory; let defaults hide the irrelevant.
- Each test builds its own data, visibly — no mystery guests.
- Inject time/random/IDs through a seam; never call
now()/random()in testable logic. - Put teardown in a hook so it runs on failure.
- Stub when feeding data, mock only when the interaction is the requirement.
- Default to fresh fixtures; run shuffled/parallel to catch interdependence.
← Interview · Craftsmanship Disciplines · Roadmap · Next: Find-Bug
In this topic