testing-patterns
Patterns for testing code effectively. Use when breaking dependencies for testability, adding tests to existing code, understanding unfamiliar code through characterization tests, or deciding how to structure tests. Covers seams, dependency injection, test doubles, and safe refactoring techniques from Michael Feathers.
What this skill does
# Testing Patterns
**Core insight**: Code without tests is hard to change safely. The Testing Dilemma: to change code safely you need tests, but to add tests you often need to change code first.
## The Seam Model
A **seam** is a place where you can alter behavior without editing the source file.
Every seam has an **enabling point** - the place where you decide which behavior to use.
### Seam Types (Best to Worst)
**Object Seams** (preferred in OO languages):
```typescript
// Original - hard dependency
class PaymentProcessor {
process(amount: number) {
const gateway = new StripeGateway(); // untestable
return gateway.charge(amount);
}
}
// With seam - injectable dependency
class PaymentProcessor {
constructor(private gateway: PaymentGateway = new StripeGateway()) {}
process(amount: number) {
return this.gateway.charge(amount); // enabling point: constructor
}
}
```
**Link Seams** (classpath/module resolution):
- Enabling point is outside program text (build scripts, import maps)
- Swap implementations at link time
- Useful but harder to notice
**Preprocessing Seams** (C/C++ only):
- `#include` and `#define` manipulation
- Last resort - avoid in modern languages
## Characterization Tests
**Purpose**: Document what code actually does, not what it should do.
**Process**:
1. Write a test you know will fail
2. Run it - let the failure tell you actual behavior
3. Change the test to expect actual behavior
4. Repeat until you've characterized the code
```typescript
// Step 1: Write failing test
test("calculateFee returns... something", () => {
const result = calculateFee(100, "premium");
expect(result).toBe(0); // will fail, tells us actual value
});
// Step 2: After failure shows "Expected 0, got 15"
test("calculateFee returns 15 for premium with 100", () => {
const result = calculateFee(100, "premium");
expect(result).toBe(15); // now documents actual behavior
});
```
**Key insight**: Characterization tests verify behaviors ARE present, enabling safe refactoring. They're not about correctness - they're about preservation.
## Breaking Dependencies
### When You Can't Instantiate a Class
**Parameterize Constructor** - externalize dependencies:
```typescript
// Before
class MailChecker {
constructor(checkPeriod: number) {
this.receiver = new MailReceiver(); // hidden dependency
}
}
// After - add parameter with default
class MailChecker {
constructor(
checkPeriod: number,
receiver: MailReceiver = new MailReceiver(),
) {
this.receiver = receiver;
}
}
```
**Extract Interface** - safest dependency break:
```typescript
// 1. Create interface from class
interface MessageReceiver {
receive(): Message[];
}
// 2. Have original implement it
class MailReceiver implements MessageReceiver { ... }
// 3. Create test double
class FakeReceiver implements MessageReceiver {
messages: Message[] = [];
receive() { return this.messages; }
}
```
**Subclass and Override Method** - core technique:
```typescript
// Production class with problematic method
class OrderProcessor {
protected getDatabase(): Database {
return new ProductionDatabase(); // can't use in tests
}
process(order: Order) {
const db = this.getDatabase();
// ... processing logic
}
}
// Testing subclass
class TestableOrderProcessor extends OrderProcessor {
protected getDatabase(): Database {
return new InMemoryDatabase(); // test-friendly
}
}
```
### Sensing vs Separation
**Sensing**: Need to verify effects of code (what did it do?)
**Separation**: Need to run code independently (isolate from dependencies)
Choose technique based on which problem you're solving.
## Adding New Behavior Safely
### Sprout Method
Add new behavior in a new method, call it from existing code:
```typescript
// Before - need to add validation
function processOrder(order: Order) {
// ... 200 lines of untested code
saveOrder(order);
}
// After - sprout new tested method
function validateOrder(order: Order): ValidationResult {
// New code, fully tested
}
function processOrder(order: Order) {
const validation = validateOrder(order); // one new line
if (!validation.valid) return;
// ... 200 lines of untested code
saveOrder(order);
}
```
### Sprout Class
When new behavior doesn't fit existing class:
```typescript
// New class for new behavior
class OrderValidator {
validate(order: Order): ValidationResult { ... }
}
// Minimal change to existing code
function processOrder(order: Order) {
const validator = new OrderValidator();
if (!validator.validate(order).valid) return;
// ... existing untested code
}
```
### Wrap Method
Rename existing method, create new method that wraps it:
```typescript
// Before
function pay(employees: Employee[]) {
for (const e of employees) {
e.pay();
}
}
// After - wrap with logging
function pay(employees: Employee[]) {
logPayment(employees); // new behavior
dispatchPay(employees); // renamed original
}
function dispatchPay(employees: Employee[]) {
for (const e of employees) {
e.pay();
}
}
```
## Finding Test Points
### Effect Sketches
Draw what a method affects:
```
method()
→ modifies field1
→ calls helper() → modifies field2
→ returns value based on field3
```
### Pinch Points
A **pinch point** is a narrow place where many effects can be detected.
Look for methods that:
- Are called by many paths
- Aggregate results from multiple operations
- Sit at natural boundaries
**Pinch points are ideal test locations** - one test covers many code paths.
### Interception Points
Where you can detect effects of changes:
1. Return values
2. Modified state (fields, globals)
3. Calls to other objects (mock/spy)
## Safe Refactoring Techniques
### Preserve Signatures
When breaking dependencies without tests:
- Copy/paste method signatures exactly
- Don't change parameter types or order
- Lean on the compiler to catch mistakes
```typescript
// Safe: copy signature exactly
function process(order: Order, options: Options): Result;
// becomes
function processInternal(order: Order, options: Options): Result;
```
### Scratch Refactoring
Refactor to understand, then throw it away:
1. Make aggressive changes to understand structure
2. Don't commit - this is exploration
3. Revert everything
4. Now you understand the code
5. Make real changes with tests
### Lean on the Compiler
Use type system as safety net:
1. Make change that should cause compile errors
2. Compiler shows all affected locations
3. Fix each location
4. If it compiles, change is likely safe
## Decision Tree
```
Need to add tests to code?
│
├─ Can you write a test for it now?
│ └─ YES → Write test, make change, done
│
└─ NO → What's blocking you?
│
├─ Can't instantiate class
│ ├─ Hidden dependency → Parameterize Constructor
│ ├─ Too many dependencies → Extract Interface
│ └─ Constructor does work → Extract and Override Factory Method
│
├─ Can't call method in test
│ ├─ Private method → Test through public interface (or make protected)
│ ├─ Side effects → Extract and Override Call
│ └─ Global state → Introduce Static Setter (carefully)
│
└─ Don't understand the code
├─ Write Characterization Tests
├─ Do Scratch Refactoring (then revert)
└─ Draw Effect Sketches
```
## Kent Beck's 4 Rules of Simple Design
From Beck (codified by Corey Haines) - in priority order:
1. **Tests Pass** - Code must work. Without this, nothing else matters.
2. **Reveals Intention** - Code should express what it does clearly.
3. **No Duplication** - DRY, but specifically _duplication of knowledge_, not just structure.
4. **Fewest Elements** - Remove anything that doesn't serve the above three.
### Test Names Should Influence API
Test names reveal design problems:
```typescript
// Bad: test name doesn't match code
test("a]live cell with 2 neighbors stays alive", () => {
const cell = new Cell(true);
cell.setNeighborCount(2);
expectRelated in Code Review
gstack
IncludedFast headless browser for QA testing and site dogfooding. Navigate pages, interact with elements, verify state, diff before/after, take annotated screenshots, test responsive layouts, forms, uploads, dialogs, and capture bug evidence. Use when asked to open or test a site, verify a deployment, dogfood a user flow, or file a bug with screenshots. (gstack)
startup-due-diligence
IncludedLegal due diligence review for seed-stage and Series A startups (US, Delaware C-Corp focus). Supports both investor and founder perspectives. Capabilities include: (1) Interactive document review and issue spotting; (2) Document request list generation; (3) Cap table and SAFE/convertible note analysis; (4) Red flag identification with severity ratings; (5) Diligence report generation. TRIGGERS: due diligence, DD, startup investment, cap table review, Series A, seed round, investor diligence, legal review startup, SAFE analysis, convertible note, 409A, founder vesting.
interview-master
IncludedThis skill should be used when the user asks to "generate interview questions", "prepare for interview", "optimize resume", "conduct mock interview", "analyze git commits for resume", "generate resume from code", "review my resume", or mentions interview preparation, career assistance, or extracting project experience from git history. Provides comprehensive interview and career development guidance for both job seekers and interviewers.
fix-issue
IncludedFixes GitHub issues using parallel analysis agents for root cause investigation, code exploration, and regression detection. Reads issue context from gh CLI, searches codebase and memory for related patterns, generates a fix with tests, and links the resolution back to the issue via PR. Includes prevention analysis to avoid recurrence. Use when debugging errors, resolving regressions, fixing bugs, or triaging issues.
sf-apex
IncludedGenerates and reviews Salesforce Apex code with 150-point scoring. TRIGGER when: user writes, reviews, or fixes Apex classes, triggers, test classes, batch/queueable/schedulable jobs, or touches .cls/.trigger files. DO NOT TRIGGER when: LWC JavaScript (use sf-lwc), Flow XML (use sf-flow), SOQL-only queries (use sf-soql), or non-Salesforce code.
swift-development
IncludedComprehensive Swift development for building, testing, and deploying iOS/macOS applications. Use when Claude needs to: (1) Build Swift packages or Xcode projects from command line, (2) Run tests with XCTest or Swift Testing framework, (3) Manage iOS simulators with simctl, (4) Handle code signing, provisioning profiles, and app distribution, (5) Format or lint Swift code with SwiftFormat/SwiftLint, (6) Work with Swift Package Manager (SPM), (7) Implement Swift 6 concurrency patterns (async/await, actors, Sendable), (8) Create SwiftUI views with MVVM architecture, (9) Set up Core Data or SwiftData persistence, or any other Swift/iOS/macOS development tasks.