Pattern catalog for "I can't write the test first" moments - recognizes common testability blockers (singletons / static dependencies, network in constructors, time / random as hidden inputs, deeply nested construction, untestable boundaries) and proposes the refactor that unblocks TDD (extract interface, dependency injection, seam, ports-and-adapters). Use as TDD coaching when an engineer is stuck on a class of code. For a catalog of what-to-test heuristics with no story use heuristic-test-design-reference, to label a change's shape before planning test effort use code-change-shape-classifier, and for conventions on writing the test well once the code is testable use test-code-conventions.
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
Restructure how the code is assembled or sequenced so a test can reach
the unit under test without rebuilding the world. Covers stuck Patterns
5, 6, and 7 of tdd-stuck-pattern-resolver.
// Stuck - chain of constructions
function processOrder(orderId: string) {
const repo = new OrderRepo(new DbConnection(new ConfigLoader(new FileReader('/etc/...'))));
return new OrderService(repo).process(orderId);
}Why it's stuck: test setup needs to construct the whole tree.
Refactor - Factory + composition root:
// Composition root (one place per app)
function buildAppContainer() {
const reader = new FileReader('/etc/...');
const config = new ConfigLoader(reader);
const conn = new DbConnection(config);
const repo = new OrderRepo(conn);
const service = new OrderService(repo);
return { service, /* others */ };
}
// Production:
const { service } = buildAppContainer();
await service.process(orderId);
// Test (just the service, with fakes):
const service = new OrderService(new FakeOrderRepo());
await service.process(orderId);Production composes once at startup; tests skip the entire chain.
// Stuck - wants to test a private helper
class OrderProcessor {
fun process(order: Order) { /* ... */ }
private fun calculateTotal(items: List<Item>): Double { /* ... */ }
}Why it's stuck: the test can't reach the private method without reflection (a code smell).
Refactor options:
Default: Test through the public interface. If calculateTotal
matters, it affects process(...)'s output; test that. Keeps tests
decoupled from implementation. Use the alternatives below only when
this default doesn't fit the situation described.
Test through the public interface (the default - use unless the conditions below apply).
Extract to a separate class with public methods - use when the private logic is genuinely independent and reused, or complex enough that public-interface tests can't pin its behaviour:
class TotalCalculator {
fun calculate(items: List<Item>): Double { /* ... */ }
}
class OrderProcessor(private val totalCalculator: TotalCalculator) {
fun process(order: Order) {
val total = totalCalculator.calculate(order.items)
// ...
}
}Then TotalCalculator is tested directly; OrderProcessor tested
with a fake.
internal (Kotlin / Scala) - escape hatch when
extraction is overkill but reflection is worse; only when the
language supports module-private visibility.// Stuck - sequential async operations
async function checkout(cart) {
const tax = await taxService.calculate(cart);
const charge = await stripe.charge(cart.total + tax);
await orderRepo.save({ cart, tax, charge });
await emailService.sendConfirmation(cart.userId);
return charge;
}Why it's stuck: mocking each await; test setup gets long.
Refactor - split into orchestrator + steps:
async function checkout(cart, deps) {
const { taxService, stripe, orderRepo, emailService } = deps;
const tax = await taxService.calculate(cart);
const charge = await stripe.charge(cart.total + tax);
await orderRepo.save({ cart, tax, charge });
await emailService.sendConfirmation(cart.userId);
return charge;
}Each deps.X is injected; tests pass per-test fakes.
For very complex async chains, consider a state machine or saga pattern - testable as state transitions, not sequential awaits.