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
Substitute a dependency by injecting it at a seam instead of reaching
for a global or a hidden source. Covers stuck Patterns 1, 2, and 3 of
tdd-stuck-pattern-resolver.
// Stuck - depends on a global database client
function processOrder(orderId) {
const order = Database.getInstance().findOrder(orderId); // singleton
// ...
}Why it's stuck: the test can't substitute a fake DB without modifying global state.
Refactor - Dependency Injection:
function processOrder(orderId, db) {
const order = db.findOrder(orderId);
// ...
}
// Test:
test('processOrder fetches the order', () => {
const fakeDb = { findOrder: () => ({ id: 1 }) };
processOrder(1, fakeDb);
});The DB is now injected; the test passes a fake. Production code
calls processOrder(orderId, Database.getInstance()) from the
single composition root.
// Stuck - constructor side-effects
class OrderService {
constructor() {
this.config = await fetch('/config').then(r => r.json()); // 😱
}
}Why it's stuck: instantiating the class to test it triggers the network call.
Refactor - push side effects out of construction:
class OrderService {
constructor(config) {
this.config = config;
}
}
// Composition root:
const config = await fetch('/config').then(r => r.json());
const orderService = new OrderService(config);
// Test:
const orderService = new OrderService({ /* fake config */ });Construction = pure assignment. Side effects happen at composition.
// Stuck - uses Date.now() and Math.random() directly
function generateInvoice(items) {
return {
id: `INV-${Date.now()}-${Math.random()}`,
items,
};
}Why it's stuck: the test can't predict the output.
Refactor - inject the source:
function generateInvoice(items, { now, rand }) {
return {
id: `INV-${now()}-${rand()}`,
items,
};
}
// Production:
generateInvoice(items, { now: Date.now, rand: Math.random });
// Test:
generateInvoice([item], { now: () => 1000, rand: () => 0.5 });
// Asserts: id === 'INV-1000-0.5'For more comprehensive control, use a Clock interface (the
same injection pattern applies to database connections).