specweave 0.23.18 → 0.24.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +144 -45
- package/CLAUDE.md +137 -4
- package/dist/src/cli/helpers/ado-area-path-mapper.d.ts +89 -0
- package/dist/src/cli/helpers/ado-area-path-mapper.d.ts.map +1 -0
- package/dist/src/cli/helpers/ado-area-path-mapper.js +213 -0
- package/dist/src/cli/helpers/ado-area-path-mapper.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/ado-auto-discover.d.ts +29 -0
- package/dist/src/cli/helpers/issue-tracker/ado-auto-discover.d.ts.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/ado-auto-discover.js +109 -0
- package/dist/src/cli/helpers/issue-tracker/ado-auto-discover.js.map +1 -0
- package/dist/src/cli/helpers/issue-tracker/ado.d.ts +1 -0
- package/dist/src/cli/helpers/issue-tracker/ado.d.ts.map +1 -1
- package/dist/src/cli/helpers/issue-tracker/ado.js +2 -0
- package/dist/src/cli/helpers/issue-tracker/ado.js.map +1 -1
- package/dist/src/cli/helpers/smart-filter.d.ts +83 -0
- package/dist/src/cli/helpers/smart-filter.d.ts.map +1 -0
- package/dist/src/cli/helpers/smart-filter.js +265 -0
- package/dist/src/cli/helpers/smart-filter.js.map +1 -0
- package/dist/src/core/qa/quality-gate-decider.d.ts +1 -1
- package/dist/src/core/qa/quality-gate-decider.js +2 -2
- package/dist/src/core/qa/quality-gate-decider.js.map +1 -1
- package/dist/src/core/qa/risk-calculator.d.ts +2 -2
- package/dist/src/core/qa/risk-calculator.js +2 -2
- package/dist/src/core/repo-structure/repo-structure-manager.d.ts.map +1 -1
- package/dist/src/core/repo-structure/repo-structure-manager.js +76 -43
- package/dist/src/core/repo-structure/repo-structure-manager.js.map +1 -1
- package/dist/src/core/validators/ac-presence-validator.d.ts +56 -0
- package/dist/src/core/validators/ac-presence-validator.d.ts.map +1 -0
- package/dist/src/core/validators/ac-presence-validator.js +149 -0
- package/dist/src/core/validators/ac-presence-validator.js.map +1 -0
- package/dist/src/integrations/ado/area-path-mapper.d.ts +137 -0
- package/dist/src/integrations/ado/area-path-mapper.d.ts.map +1 -0
- package/dist/src/integrations/ado/area-path-mapper.js +267 -0
- package/dist/src/integrations/ado/area-path-mapper.js.map +1 -0
- package/dist/src/integrations/jira/filter-processor.d.ts +126 -0
- package/dist/src/integrations/jira/filter-processor.d.ts.map +1 -0
- package/dist/src/integrations/jira/filter-processor.js +207 -0
- package/dist/src/integrations/jira/filter-processor.js.map +1 -0
- package/dist/src/integrations/jira/jira-client.d.ts +13 -0
- package/dist/src/integrations/jira/jira-client.d.ts.map +1 -1
- package/dist/src/integrations/jira/jira-client.js +33 -0
- package/dist/src/integrations/jira/jira-client.js.map +1 -1
- package/dist/src/utils/ac-embedder.d.ts +63 -0
- package/dist/src/utils/ac-embedder.d.ts.map +1 -0
- package/dist/src/utils/ac-embedder.js +217 -0
- package/dist/src/utils/ac-embedder.js.map +1 -0
- package/dist/src/utils/env-manager.d.ts +86 -0
- package/dist/src/utils/env-manager.d.ts.map +1 -0
- package/dist/src/utils/env-manager.js +188 -0
- package/dist/src/utils/env-manager.js.map +1 -0
- package/package.json +1 -1
- package/plugins/specweave/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave/agents/AGENTS-INDEX.md +1 -1
- package/plugins/specweave/agents/increment-quality-judge-v2/AGENT.md +9 -9
- package/plugins/specweave/commands/specweave-do.md +37 -0
- package/plugins/specweave/commands/specweave-done.md +159 -0
- package/plugins/specweave/commands/specweave-embed-acs.md +446 -0
- package/plugins/specweave/commands/specweave-next.md +148 -3
- package/plugins/specweave/commands/specweave-qa.md +2 -2
- package/plugins/specweave/hooks/pre-increment-start.sh +168 -0
- package/plugins/specweave/skills/SKILLS-INDEX.md +1 -1
- package/plugins/specweave-ado/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-ado/commands/specweave-ado-import-projects.md +331 -0
- package/plugins/specweave-alternatives/.claude-plugin/plugin.json +10 -0
- package/plugins/specweave-alternatives/commands/alternatives-analyze.md +336 -0
- package/plugins/specweave-alternatives/skills/architecture-alternatives/SKILL.md +651 -0
- package/plugins/specweave-alternatives/skills/bmad-method/SKILL.md +420 -0
- package/plugins/specweave-alternatives/skills/spec-kit-expert/SKILL.md +487 -0
- package/plugins/specweave-backend/commands/api-scaffold.md +80 -0
- package/plugins/specweave-backend/commands/crud-generate.md +109 -0
- package/plugins/specweave-backend/commands/migration-generate.md +139 -0
- package/plugins/specweave-confluent/commands/connector-deploy.md +154 -0
- package/plugins/specweave-confluent/commands/ksqldb-query.md +179 -0
- package/plugins/specweave-confluent/commands/schema-register.md +123 -0
- package/plugins/specweave-core/.claude-plugin/plugin.json +21 -0
- package/plugins/specweave-core/commands/architecture-review.md +288 -0
- package/plugins/specweave-core/commands/code-review.md +213 -0
- package/plugins/specweave-core/commands/refactor-plan.md +249 -0
- package/plugins/specweave-core/skills/code-quality/SKILL.md +157 -0
- package/plugins/specweave-core/skills/design-patterns/SKILL.md +244 -0
- package/plugins/specweave-core/skills/software-architecture/SKILL.md +83 -0
- package/plugins/specweave-cost-optimizer/.claude-plugin/plugin.json +22 -0
- package/plugins/specweave-cost-optimizer/commands/cost-analyze.md +360 -0
- package/plugins/specweave-cost-optimizer/commands/cost-optimize.md +480 -0
- package/plugins/specweave-cost-optimizer/skills/aws-cost-expert/SKILL.md +416 -0
- package/plugins/specweave-cost-optimizer/skills/cloud-pricing/SKILL.md +325 -0
- package/plugins/specweave-cost-optimizer/skills/cost-optimization/SKILL.md +337 -0
- package/plugins/specweave-diagrams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-diagrams/commands/diagrams-generate.md +168 -0
- package/plugins/specweave-docs/.claude-plugin/plugin.json +10 -0
- package/plugins/specweave-docs/commands/docs-generate.md +441 -0
- package/plugins/specweave-docs/commands/docs-init.md +334 -0
- package/plugins/specweave-docs/skills/docusaurus/SKILL.md +581 -0
- package/plugins/specweave-docs/skills/spec-driven-brainstorming/SKILL.md +689 -0
- package/plugins/specweave-docs/skills/technical-writing/SKILL.md +1039 -0
- package/plugins/specweave-docs-preview/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-figma/.claude-plugin/plugin.json +23 -0
- package/plugins/specweave-figma/commands/figma-import.md +690 -0
- package/plugins/specweave-figma/commands/figma-to-react.md +834 -0
- package/plugins/specweave-figma/commands/figma-tokens.md +815 -0
- package/plugins/specweave-frontend/.claude-plugin/plugin.json +21 -0
- package/plugins/specweave-frontend/agents/frontend-architect/AGENT.md +408 -0
- package/plugins/specweave-frontend/agents/frontend-architect/README.md +385 -0
- package/plugins/specweave-frontend/agents/frontend-architect/examples.md +590 -0
- package/plugins/specweave-frontend/agents/frontend-architect/templates/component-template.tsx +152 -0
- package/plugins/specweave-frontend/agents/frontend-architect/templates/hook-template.ts +311 -0
- package/plugins/specweave-frontend/agents/frontend-architect/templates/page-template.tsx +228 -0
- package/plugins/specweave-frontend/commands/component-generate.md +510 -0
- package/plugins/specweave-frontend/commands/design-system-init.md +494 -0
- package/plugins/specweave-frontend/commands/frontend-scaffold.md +207 -0
- package/plugins/specweave-frontend/commands/nextjs-setup.md +396 -0
- package/plugins/specweave-frontend/skills/design-system-architect/SKILL.md +278 -0
- package/plugins/specweave-frontend/skills/frontend/SKILL.md +420 -0
- package/plugins/specweave-frontend/skills/nextjs/SKILL.md +546 -0
- package/plugins/specweave-github/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-github/hooks/.specweave/logs/hooks-debug.log +212 -0
- package/plugins/specweave-infrastructure/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-jira/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-jira/commands/import-projects.js +183 -0
- package/plugins/specweave-jira/commands/import-projects.md +97 -0
- package/plugins/specweave-jira/commands/import-projects.ts +288 -0
- package/plugins/specweave-jira/commands/specweave-jira-import-projects.md +298 -0
- package/plugins/specweave-kafka/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kafka-streams/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-kubernetes/commands/cluster-setup.md +262 -0
- package/plugins/specweave-kubernetes/commands/deployment-generate.md +242 -0
- package/plugins/specweave-kubernetes/commands/helm-scaffold.md +333 -0
- package/plugins/specweave-ml/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-mobile/commands/app-scaffold.md +233 -0
- package/plugins/specweave-mobile/commands/build-config.md +256 -0
- package/plugins/specweave-mobile/commands/screen-generate.md +289 -0
- package/plugins/specweave-n8n/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-payments/commands/stripe-setup.md +931 -0
- package/plugins/specweave-payments/commands/subscription-flow.md +1193 -0
- package/plugins/specweave-payments/commands/subscription-manage.md +386 -0
- package/plugins/specweave-payments/commands/webhook-setup.md +295 -0
- package/plugins/specweave-plugin-dev/.claude-plugin/plugin.json +13 -12
- package/plugins/specweave-plugin-dev/commands/plugin-create.md +333 -0
- package/plugins/specweave-plugin-dev/commands/plugin-publish.md +339 -0
- package/plugins/specweave-plugin-dev/commands/plugin-test.md +293 -0
- package/plugins/specweave-plugin-dev/skills/claude-sdk/SKILL.md +162 -0
- package/plugins/specweave-plugin-dev/skills/marketplace-publishing/SKILL.md +263 -0
- package/plugins/specweave-plugin-dev/skills/plugin-development/SKILL.md +316 -0
- package/plugins/specweave-release/.claude-plugin/plugin.json +1 -1
- package/plugins/specweave-release/commands/specweave-release-npm.md +110 -0
- package/plugins/specweave-release/hooks/.specweave/logs/dora-tracking.log +168 -0
- package/plugins/specweave-testing/.claude-plugin/plugin.json +21 -0
- package/plugins/specweave-testing/agents/qa-engineer/AGENT.md +818 -0
- package/plugins/specweave-testing/agents/qa-engineer/README.md +443 -0
- package/plugins/specweave-testing/agents/qa-engineer/templates/playwright-e2e-test.ts +470 -0
- package/plugins/specweave-testing/agents/qa-engineer/templates/test-data-factory.ts +507 -0
- package/plugins/specweave-testing/agents/qa-engineer/templates/vitest-unit-test.ts +400 -0
- package/plugins/specweave-testing/agents/qa-engineer/test-strategies.md +726 -0
- package/plugins/specweave-testing/commands/e2e-setup.md +1081 -0
- package/plugins/specweave-testing/commands/test-coverage.md +979 -0
- package/plugins/specweave-testing/commands/test-generate.md +1156 -0
- package/plugins/specweave-testing/commands/test-init.md +409 -0
- package/plugins/specweave-testing/skills/e2e-playwright/SKILL.md +769 -0
- package/plugins/specweave-testing/skills/tdd-expert/SKILL.md +934 -0
- package/plugins/specweave-testing/skills/unit-testing-expert/SKILL.md +1011 -0
- package/plugins/specweave-tooling/.claude-plugin/plugin.json +22 -0
- package/plugins/specweave-tooling/commands/specweave-tooling-skill-create.md +691 -0
- package/plugins/specweave-tooling/commands/specweave-tooling-skill-package.md +751 -0
- package/plugins/specweave-tooling/commands/specweave-tooling-skill-validate.md +858 -0
- package/plugins/specweave-ui/.claude-plugin/plugin.json +10 -0
- package/plugins/specweave-ui/commands/ui-automate.md +199 -0
- package/plugins/specweave-ui/commands/ui-inspect.md +70 -0
- package/plugins/specweave-ui/skills/browser-automation/SKILL.md +314 -0
- package/plugins/specweave-ui/skills/ui-testing/SKILL.md +716 -0
- package/plugins/specweave-ui/skills/visual-regression/SKILL.md +728 -0
- package/plugins/specweave/commands/check-hooks.md +0 -257
- package/plugins/specweave/commands/specweave-archive-increments.md +0 -82
- package/plugins/specweave-plugin-dev/skills/plugin-expert/SKILL.md +0 -1231
- /package/plugins/specweave/{agents/code-reviewer.md → skills/code-reviewer/SKILL.md} +0 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tdd-expert
|
|
3
|
+
description: Test-Driven Development (TDD) expertise covering red-green-refactor cycle, behavior-driven development, test-first design, refactoring with confidence, TDD best practices, TDD workflow, unit testing strategies, mock-driven development, test doubles, TDD patterns, SOLID principles through testing, emergent design, incremental development, TDD anti-patterns, and production-grade TDD practices. Activates for TDD, test-driven development, red-green-refactor, test-first, behavior-driven, BDD, refactoring, test doubles, mock-driven, test design, SOLID principles, emergent design, incremental development, TDD workflow, TDD best practices, TDD patterns, Kent Beck, Robert Martin, Uncle Bob, test-first design.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Test-Driven Development (TDD) Expert
|
|
7
|
+
|
|
8
|
+
## Core Philosophy
|
|
9
|
+
|
|
10
|
+
Test-Driven Development is a software development approach where tests are written BEFORE the implementation code. This forces better design, ensures testability, and provides a safety net for refactoring.
|
|
11
|
+
|
|
12
|
+
**Core Principle**: "Red, Green, Refactor"
|
|
13
|
+
|
|
14
|
+
## The TDD Cycle
|
|
15
|
+
|
|
16
|
+
### 1. Red Phase: Write a Failing Test
|
|
17
|
+
|
|
18
|
+
**Goal**: Define expected behavior through a test that fails
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { Calculator } from './Calculator';
|
|
23
|
+
|
|
24
|
+
describe('Calculator', () => {
|
|
25
|
+
it('should add two numbers', () => {
|
|
26
|
+
const calculator = new Calculator();
|
|
27
|
+
|
|
28
|
+
// This test WILL fail because Calculator doesn't exist yet
|
|
29
|
+
expect(calculator.add(2, 3)).toBe(5);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Red Phase Checklist**:
|
|
35
|
+
- [ ] Test describes ONE specific behavior
|
|
36
|
+
- [ ] Test fails for the RIGHT reason (not syntax error)
|
|
37
|
+
- [ ] Test is readable and understandable
|
|
38
|
+
- [ ] Expected behavior is clear from test name
|
|
39
|
+
|
|
40
|
+
**Common Mistakes in Red Phase**:
|
|
41
|
+
- Writing multiple assertions in one test
|
|
42
|
+
- Testing implementation details instead of behavior
|
|
43
|
+
- Writing tests that pass immediately (no value!)
|
|
44
|
+
- Unclear test names
|
|
45
|
+
|
|
46
|
+
### 2. Green Phase: Make It Pass (Minimal Implementation)
|
|
47
|
+
|
|
48
|
+
**Goal**: Write the simplest code that makes the test pass
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Calculator.ts
|
|
52
|
+
export class Calculator {
|
|
53
|
+
add(a: number, b: number): number {
|
|
54
|
+
return 5; // Hardcoded! But test passes
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Wait, hardcoded 5?** Yes! This is intentional. The green phase is about making tests pass with minimal code. The next test will force us to generalize.
|
|
60
|
+
|
|
61
|
+
**Add another test** (triangulation):
|
|
62
|
+
```typescript
|
|
63
|
+
it('should add different numbers', () => {
|
|
64
|
+
const calculator = new Calculator();
|
|
65
|
+
expect(calculator.add(10, 20)).toBe(30); // Now hardcoded 5 fails!
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Now implement properly**:
|
|
70
|
+
```typescript
|
|
71
|
+
export class Calculator {
|
|
72
|
+
add(a: number, b: number): number {
|
|
73
|
+
return a + b; // Generalized solution
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
**Green Phase Checklist**:
|
|
79
|
+
- [ ] All tests pass
|
|
80
|
+
- [ ] Implementation is minimal (no premature optimization)
|
|
81
|
+
- [ ] No extra features beyond what tests require
|
|
82
|
+
- [ ] Code is simple and direct
|
|
83
|
+
|
|
84
|
+
**Common Mistakes in Green Phase**:
|
|
85
|
+
- Over-engineering the solution
|
|
86
|
+
- Adding features not covered by tests
|
|
87
|
+
- Premature optimization
|
|
88
|
+
- Skipping the refactor phase
|
|
89
|
+
|
|
90
|
+
### 3. Refactor Phase: Improve Code Quality
|
|
91
|
+
|
|
92
|
+
**Goal**: Improve code structure while keeping all tests green
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
// Before refactoring
|
|
96
|
+
export class Calculator {
|
|
97
|
+
add(a: number, b: number): number {
|
|
98
|
+
return a + b;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
subtract(a: number, b: number): number {
|
|
102
|
+
return a - b;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
multiply(a: number, b: number): number {
|
|
106
|
+
return a * b;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
divide(a: number, b: number): number {
|
|
110
|
+
if (b === 0) {
|
|
111
|
+
throw new Error('Division by zero');
|
|
112
|
+
}
|
|
113
|
+
return a / b;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
**Refactored with better design**:
|
|
119
|
+
```typescript
|
|
120
|
+
// Extract operation interface
|
|
121
|
+
interface Operation {
|
|
122
|
+
execute(a: number, b: number): number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class AddOperation implements Operation {
|
|
126
|
+
execute(a: number, b: number): number {
|
|
127
|
+
return a + b;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
class DivideOperation implements Operation {
|
|
132
|
+
execute(a: number, b: number): number {
|
|
133
|
+
if (b === 0) {
|
|
134
|
+
throw new Error('Division by zero');
|
|
135
|
+
}
|
|
136
|
+
return a / b;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export class Calculator {
|
|
141
|
+
private operations: Map<string, Operation> = new Map([
|
|
142
|
+
['add', new AddOperation()],
|
|
143
|
+
['divide', new DivideOperation()],
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
perform(operation: string, a: number, b: number): number {
|
|
147
|
+
const op = this.operations.get(operation);
|
|
148
|
+
if (!op) {
|
|
149
|
+
throw new Error(`Unknown operation: ${operation}`);
|
|
150
|
+
}
|
|
151
|
+
return op.execute(a, b);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Refactor Phase Checklist**:
|
|
157
|
+
- [ ] All tests still pass
|
|
158
|
+
- [ ] Code is more readable
|
|
159
|
+
- [ ] Duplication removed (DRY principle)
|
|
160
|
+
- [ ] Code follows SOLID principles
|
|
161
|
+
- [ ] Better separation of concerns
|
|
162
|
+
- [ ] No new functionality added
|
|
163
|
+
|
|
164
|
+
**Refactoring Patterns**:
|
|
165
|
+
- Extract Method/Function
|
|
166
|
+
- Extract Class
|
|
167
|
+
- Introduce Parameter Object
|
|
168
|
+
- Replace Conditional with Polymorphism
|
|
169
|
+
- Extract Interface
|
|
170
|
+
- Rename for clarity
|
|
171
|
+
|
|
172
|
+
## TDD Best Practices
|
|
173
|
+
|
|
174
|
+
### 1. Test Behavior, Not Implementation
|
|
175
|
+
|
|
176
|
+
**❌ BAD: Testing implementation details**
|
|
177
|
+
```typescript
|
|
178
|
+
it('should call internal helper method', () => {
|
|
179
|
+
const spy = vi.spyOn(calculator, '_internalHelper');
|
|
180
|
+
calculator.add(2, 3);
|
|
181
|
+
expect(spy).toHaveBeenCalled(); // Brittle! Breaks on refactor
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**✅ GOOD: Testing behavior**
|
|
186
|
+
```typescript
|
|
187
|
+
it('should return sum of two numbers', () => {
|
|
188
|
+
expect(calculator.add(2, 3)).toBe(5); // Robust!
|
|
189
|
+
});
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 2. One Assertion Per Test (When Possible)
|
|
193
|
+
|
|
194
|
+
**❌ BAD: Multiple unrelated assertions**
|
|
195
|
+
```typescript
|
|
196
|
+
it('should validate user', () => {
|
|
197
|
+
expect(user.name).toBe('John');
|
|
198
|
+
expect(user.email).toBe('john@example.com');
|
|
199
|
+
expect(user.isActive).toBe(true);
|
|
200
|
+
expect(user.roles).toContain('admin');
|
|
201
|
+
});
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**✅ GOOD: Focused tests**
|
|
205
|
+
```typescript
|
|
206
|
+
it('should have correct name', () => {
|
|
207
|
+
expect(user.name).toBe('John');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should have valid email', () => {
|
|
211
|
+
expect(user.email).toBe('john@example.com');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('should be active by default', () => {
|
|
215
|
+
expect(user.isActive).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('should include admin role', () => {
|
|
219
|
+
expect(user.roles).toContain('admin');
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 3. Test Edge Cases and Boundaries
|
|
224
|
+
|
|
225
|
+
**Test pyramid for a single function**:
|
|
226
|
+
1. Happy path (normal input)
|
|
227
|
+
2. Edge cases (empty, null, boundary values)
|
|
228
|
+
3. Error cases (invalid input)
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
describe('divide', () => {
|
|
232
|
+
// Happy path
|
|
233
|
+
it('should divide two positive numbers', () => {
|
|
234
|
+
expect(calculator.divide(10, 2)).toBe(5);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Edge cases
|
|
238
|
+
it('should handle division resulting in decimal', () => {
|
|
239
|
+
expect(calculator.divide(5, 2)).toBe(2.5);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('should handle negative numbers', () => {
|
|
243
|
+
expect(calculator.divide(-10, 2)).toBe(-5);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('should handle zero dividend', () => {
|
|
247
|
+
expect(calculator.divide(0, 5)).toBe(0);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// Error case
|
|
251
|
+
it('should throw error for division by zero', () => {
|
|
252
|
+
expect(() => calculator.divide(10, 0)).toThrow('Division by zero');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 4. Use Descriptive Test Names
|
|
258
|
+
|
|
259
|
+
**Pattern**: `should [expected behavior] when [condition]`
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
// ✅ GOOD: Clear and descriptive
|
|
263
|
+
it('should return empty array when no users exist', () => { ... });
|
|
264
|
+
it('should throw ValidationError when email is invalid', () => { ... });
|
|
265
|
+
it('should apply discount when user has premium membership', () => { ... });
|
|
266
|
+
|
|
267
|
+
// ❌ BAD: Vague and unclear
|
|
268
|
+
it('works', () => { ... });
|
|
269
|
+
it('test user creation', () => { ... });
|
|
270
|
+
it('returns data', () => { ... });
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 5. Follow AAA Pattern (Arrange-Act-Assert)
|
|
274
|
+
|
|
275
|
+
```typescript
|
|
276
|
+
it('should calculate total with discount', () => {
|
|
277
|
+
// ARRANGE: Set up test data and dependencies
|
|
278
|
+
const cart = new ShoppingCart();
|
|
279
|
+
cart.addItem({ name: 'Book', price: 20 });
|
|
280
|
+
cart.addItem({ name: 'Pen', price: 5 });
|
|
281
|
+
const discountCode = 'SAVE10';
|
|
282
|
+
|
|
283
|
+
// ACT: Execute the behavior under test
|
|
284
|
+
const total = cart.calculateTotal(discountCode);
|
|
285
|
+
|
|
286
|
+
// ASSERT: Verify the result
|
|
287
|
+
expect(total).toBe(22.5); // 25 * 0.9 = 22.5
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### 6. Test Isolation (No Shared State)
|
|
292
|
+
|
|
293
|
+
**❌ BAD: Shared state between tests**
|
|
294
|
+
```typescript
|
|
295
|
+
let user: User; // SHARED STATE!
|
|
296
|
+
|
|
297
|
+
beforeAll(() => {
|
|
298
|
+
user = new User('John'); // Created once
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should update name', () => {
|
|
302
|
+
user.updateName('Jane'); // MUTATES SHARED STATE
|
|
303
|
+
expect(user.name).toBe('Jane');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should have original name', () => {
|
|
307
|
+
expect(user.name).toBe('John'); // FAILS! Name is 'Jane' from previous test
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
**✅ GOOD: Isolated tests**
|
|
312
|
+
```typescript
|
|
313
|
+
describe('User', () => {
|
|
314
|
+
let user: User;
|
|
315
|
+
|
|
316
|
+
beforeEach(() => {
|
|
317
|
+
user = new User('John'); // FRESH instance for each test
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should update name', () => {
|
|
321
|
+
user.updateName('Jane');
|
|
322
|
+
expect(user.name).toBe('Jane');
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
it('should have original name', () => {
|
|
326
|
+
expect(user.name).toBe('John'); // PASSES! Fresh instance
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## TDD Workflow Examples
|
|
332
|
+
|
|
333
|
+
### Example 1: Building a User Validator
|
|
334
|
+
|
|
335
|
+
**Step 1: RED - Write failing test**
|
|
336
|
+
```typescript
|
|
337
|
+
describe('UserValidator', () => {
|
|
338
|
+
it('should validate email format', () => {
|
|
339
|
+
const validator = new UserValidator();
|
|
340
|
+
expect(validator.validateEmail('user@example.com')).toBe(true);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Step 2: GREEN - Minimal implementation**
|
|
346
|
+
```typescript
|
|
347
|
+
export class UserValidator {
|
|
348
|
+
validateEmail(email: string): boolean {
|
|
349
|
+
return true; // Hardcoded! But test passes
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
**Step 3: Add negative test (triangulation)**
|
|
355
|
+
```typescript
|
|
356
|
+
it('should reject invalid email', () => {
|
|
357
|
+
const validator = new UserValidator();
|
|
358
|
+
expect(validator.validateEmail('invalid')).toBe(false); // Forces real implementation
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
**Step 4: GREEN - Real implementation**
|
|
363
|
+
```typescript
|
|
364
|
+
export class UserValidator {
|
|
365
|
+
validateEmail(email: string): boolean {
|
|
366
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
**Step 5: REFACTOR - Extract regex**
|
|
372
|
+
```typescript
|
|
373
|
+
export class UserValidator {
|
|
374
|
+
private static EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
375
|
+
|
|
376
|
+
validateEmail(email: string): boolean {
|
|
377
|
+
return UserValidator.EMAIL_REGEX.test(email);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
**Step 6: Add more edge cases**
|
|
383
|
+
```typescript
|
|
384
|
+
it('should reject empty email', () => {
|
|
385
|
+
expect(validator.validateEmail('')).toBe(false);
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
it('should reject null email', () => {
|
|
389
|
+
expect(validator.validateEmail(null as any)).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
it('should reject email without domain', () => {
|
|
393
|
+
expect(validator.validateEmail('user@')).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
**Final refactored implementation**:
|
|
398
|
+
```typescript
|
|
399
|
+
export class UserValidator {
|
|
400
|
+
private static EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
401
|
+
|
|
402
|
+
validateEmail(email: string | null | undefined): boolean {
|
|
403
|
+
if (!email || typeof email !== 'string') {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
return UserValidator.EMAIL_REGEX.test(email);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
### Example 2: Building a Shopping Cart
|
|
412
|
+
|
|
413
|
+
**RED: Test adding items**
|
|
414
|
+
```typescript
|
|
415
|
+
describe('ShoppingCart', () => {
|
|
416
|
+
it('should add item to cart', () => {
|
|
417
|
+
const cart = new ShoppingCart();
|
|
418
|
+
cart.addItem({ id: 1, name: 'Book', price: 20 });
|
|
419
|
+
|
|
420
|
+
expect(cart.getItemCount()).toBe(1);
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
**GREEN: Minimal implementation**
|
|
426
|
+
```typescript
|
|
427
|
+
interface CartItem {
|
|
428
|
+
id: number;
|
|
429
|
+
name: string;
|
|
430
|
+
price: number;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
export class ShoppingCart {
|
|
434
|
+
private items: CartItem[] = [];
|
|
435
|
+
|
|
436
|
+
addItem(item: CartItem): void {
|
|
437
|
+
this.items.push(item);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
getItemCount(): number {
|
|
441
|
+
return this.items.length;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**RED: Test calculating total**
|
|
447
|
+
```typescript
|
|
448
|
+
it('should calculate total price', () => {
|
|
449
|
+
const cart = new ShoppingCart();
|
|
450
|
+
cart.addItem({ id: 1, name: 'Book', price: 20 });
|
|
451
|
+
cart.addItem({ id: 2, name: 'Pen', price: 5 });
|
|
452
|
+
|
|
453
|
+
expect(cart.getTotal()).toBe(25);
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
**GREEN: Implement total**
|
|
458
|
+
```typescript
|
|
459
|
+
export class ShoppingCart {
|
|
460
|
+
// ... previous code ...
|
|
461
|
+
|
|
462
|
+
getTotal(): number {
|
|
463
|
+
return this.items.reduce((sum, item) => sum + item.price, 0);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
**RED: Test removing items**
|
|
469
|
+
```typescript
|
|
470
|
+
it('should remove item from cart', () => {
|
|
471
|
+
const cart = new ShoppingCart();
|
|
472
|
+
cart.addItem({ id: 1, name: 'Book', price: 20 });
|
|
473
|
+
cart.removeItem(1);
|
|
474
|
+
|
|
475
|
+
expect(cart.getItemCount()).toBe(0);
|
|
476
|
+
});
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**GREEN: Implement remove**
|
|
480
|
+
```typescript
|
|
481
|
+
export class ShoppingCart {
|
|
482
|
+
// ... previous code ...
|
|
483
|
+
|
|
484
|
+
removeItem(itemId: number): void {
|
|
485
|
+
this.items = this.items.filter(item => item.id !== itemId);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**REFACTOR: Extract calculations**
|
|
491
|
+
```typescript
|
|
492
|
+
export class ShoppingCart {
|
|
493
|
+
private items: CartItem[] = [];
|
|
494
|
+
|
|
495
|
+
addItem(item: CartItem): void {
|
|
496
|
+
this.items.push(item);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
removeItem(itemId: number): void {
|
|
500
|
+
this.items = this.items.filter(item => item.id !== itemId);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
getItemCount(): number {
|
|
504
|
+
return this.items.length;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
getTotal(): number {
|
|
508
|
+
return this.calculateTotal(this.items);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private calculateTotal(items: CartItem[]): number {
|
|
512
|
+
return items.reduce((sum, item) => sum + item.price, 0);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
isEmpty(): boolean {
|
|
516
|
+
return this.items.length === 0;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
## TDD with Mocks and Test Doubles
|
|
522
|
+
|
|
523
|
+
### Types of Test Doubles
|
|
524
|
+
|
|
525
|
+
1. **Dummy**: Objects passed but never used
|
|
526
|
+
2. **Stub**: Returns predefined values
|
|
527
|
+
3. **Spy**: Records information about calls
|
|
528
|
+
4. **Mock**: Verifies interactions
|
|
529
|
+
5. **Fake**: Working implementation (simplified)
|
|
530
|
+
|
|
531
|
+
### Example: Testing with Mocks
|
|
532
|
+
|
|
533
|
+
**RED: Test user creation with email service**
|
|
534
|
+
```typescript
|
|
535
|
+
describe('UserService', () => {
|
|
536
|
+
it('should send welcome email when user created', async () => {
|
|
537
|
+
const emailService = createMockEmailService();
|
|
538
|
+
const userService = new UserService(emailService);
|
|
539
|
+
|
|
540
|
+
await userService.createUser({ name: 'John', email: 'john@example.com' });
|
|
541
|
+
|
|
542
|
+
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith('john@example.com');
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**GREEN: Implementation with dependency injection**
|
|
548
|
+
```typescript
|
|
549
|
+
interface EmailService {
|
|
550
|
+
sendWelcomeEmail(email: string): Promise<void>;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export class UserService {
|
|
554
|
+
constructor(private emailService: EmailService) {}
|
|
555
|
+
|
|
556
|
+
async createUser(userData: { name: string; email: string }): Promise<User> {
|
|
557
|
+
const user = new User(userData);
|
|
558
|
+
await this.emailService.sendWelcomeEmail(user.email);
|
|
559
|
+
return user;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Test helper
|
|
564
|
+
function createMockEmailService(): EmailService {
|
|
565
|
+
return {
|
|
566
|
+
sendWelcomeEmail: vi.fn().mockResolvedValue(undefined),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## TDD and SOLID Principles
|
|
572
|
+
|
|
573
|
+
TDD naturally leads to SOLID design:
|
|
574
|
+
|
|
575
|
+
### 1. Single Responsibility Principle (SRP)
|
|
576
|
+
|
|
577
|
+
**TDD forces SRP** because classes with multiple responsibilities are hard to test.
|
|
578
|
+
|
|
579
|
+
```typescript
|
|
580
|
+
// ❌ BAD: Multiple responsibilities (hard to test!)
|
|
581
|
+
class UserManager {
|
|
582
|
+
createUser() { /* ... */ }
|
|
583
|
+
sendEmail() { /* ... */ }
|
|
584
|
+
saveToDatabase() { /* ... */ }
|
|
585
|
+
validateInput() { /* ... */ }
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ✅ GOOD: Single responsibility (easy to test!)
|
|
589
|
+
class UserCreator {
|
|
590
|
+
createUser() { /* ... */ }
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
class EmailSender {
|
|
594
|
+
sendEmail() { /* ... */ }
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
class UserRepository {
|
|
598
|
+
save() { /* ... */ }
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
class UserValidator {
|
|
602
|
+
validate() { /* ... */ }
|
|
603
|
+
}
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### 2. Open/Closed Principle (OCP)
|
|
607
|
+
|
|
608
|
+
**TDD encourages extension through abstraction**
|
|
609
|
+
|
|
610
|
+
```typescript
|
|
611
|
+
// Extension through interfaces
|
|
612
|
+
interface PaymentProcessor {
|
|
613
|
+
process(amount: number): Promise<PaymentResult>;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
class StripeProcessor implements PaymentProcessor {
|
|
617
|
+
async process(amount: number): Promise<PaymentResult> {
|
|
618
|
+
// Stripe implementation
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
class PayPalProcessor implements PaymentProcessor {
|
|
623
|
+
async process(amount: number): Promise<PaymentResult> {
|
|
624
|
+
// PayPal implementation
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Tests can easily mock PaymentProcessor
|
|
629
|
+
describe('OrderService', () => {
|
|
630
|
+
it('should process payment', async () => {
|
|
631
|
+
const mockProcessor: PaymentProcessor = {
|
|
632
|
+
process: vi.fn().mockResolvedValue({ success: true }),
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
const orderService = new OrderService(mockProcessor);
|
|
636
|
+
await orderService.completeOrder(100);
|
|
637
|
+
|
|
638
|
+
expect(mockProcessor.process).toHaveBeenCalledWith(100);
|
|
639
|
+
});
|
|
640
|
+
});
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### 3. Liskov Substitution Principle (LSP)
|
|
644
|
+
|
|
645
|
+
**TDD ensures LSP through contract testing**
|
|
646
|
+
|
|
647
|
+
```typescript
|
|
648
|
+
// Base class contract
|
|
649
|
+
abstract class Shape {
|
|
650
|
+
abstract area(): number;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// Implementations must satisfy contract
|
|
654
|
+
class Rectangle extends Shape {
|
|
655
|
+
constructor(private width: number, private height: number) {
|
|
656
|
+
super();
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
area(): number {
|
|
660
|
+
return this.width * this.height;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
class Circle extends Shape {
|
|
665
|
+
constructor(private radius: number) {
|
|
666
|
+
super();
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
area(): number {
|
|
670
|
+
return Math.PI * this.radius ** 2;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// Test contract for all shapes
|
|
675
|
+
function testShapeContract(createShape: () => Shape) {
|
|
676
|
+
it('should have positive area', () => {
|
|
677
|
+
const shape = createShape();
|
|
678
|
+
expect(shape.area()).toBeGreaterThan(0);
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it('should return number', () => {
|
|
682
|
+
const shape = createShape();
|
|
683
|
+
expect(typeof shape.area()).toBe('number');
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
describe('Rectangle', () => {
|
|
688
|
+
testShapeContract(() => new Rectangle(10, 5));
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
describe('Circle', () => {
|
|
692
|
+
testShapeContract(() => new Circle(5));
|
|
693
|
+
});
|
|
694
|
+
```
|
|
695
|
+
|
|
696
|
+
### 4. Interface Segregation Principle (ISP)
|
|
697
|
+
|
|
698
|
+
**TDD reveals when interfaces are too large**
|
|
699
|
+
|
|
700
|
+
```typescript
|
|
701
|
+
// ❌ BAD: Fat interface (forces unnecessary mocking)
|
|
702
|
+
interface Worker {
|
|
703
|
+
work(): void;
|
|
704
|
+
eat(): void;
|
|
705
|
+
sleep(): void;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Mocking is painful
|
|
709
|
+
const mockWorker: Worker = {
|
|
710
|
+
work: vi.fn(),
|
|
711
|
+
eat: vi.fn(), // Not needed for this test!
|
|
712
|
+
sleep: vi.fn(), // Not needed for this test!
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// ✅ GOOD: Segregated interfaces
|
|
716
|
+
interface Workable {
|
|
717
|
+
work(): void;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
interface Eatable {
|
|
721
|
+
eat(): void;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Only mock what you need
|
|
725
|
+
const mockWorkable: Workable = {
|
|
726
|
+
work: vi.fn(),
|
|
727
|
+
};
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### 5. Dependency Inversion Principle (DIP)
|
|
731
|
+
|
|
732
|
+
**TDD requires dependency injection (essential for testing)**
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
// ✅ GOOD: Depends on abstraction
|
|
736
|
+
interface Logger {
|
|
737
|
+
log(message: string): void;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
class UserService {
|
|
741
|
+
constructor(private logger: Logger) {} // Injected dependency
|
|
742
|
+
|
|
743
|
+
createUser(name: string) {
|
|
744
|
+
this.logger.log(`Creating user: ${name}`);
|
|
745
|
+
// ...
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Easy to test with mock
|
|
750
|
+
describe('UserService', () => {
|
|
751
|
+
it('should log user creation', () => {
|
|
752
|
+
const mockLogger: Logger = { log: vi.fn() };
|
|
753
|
+
const service = new UserService(mockLogger);
|
|
754
|
+
|
|
755
|
+
service.createUser('John');
|
|
756
|
+
|
|
757
|
+
expect(mockLogger.log).toHaveBeenCalledWith('Creating user: John');
|
|
758
|
+
});
|
|
759
|
+
});
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
## TDD Anti-Patterns to Avoid
|
|
763
|
+
|
|
764
|
+
### 1. The Liar (Passing test that doesn't test anything)
|
|
765
|
+
|
|
766
|
+
**❌ BAD**:
|
|
767
|
+
```typescript
|
|
768
|
+
it('should validate user', () => {
|
|
769
|
+
const result = true; // Hardcoded!
|
|
770
|
+
expect(result).toBe(true); // Always passes
|
|
771
|
+
});
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
### 2. Excessive Setup (Tests too complex)
|
|
775
|
+
|
|
776
|
+
**❌ BAD**:
|
|
777
|
+
```typescript
|
|
778
|
+
beforeEach(() => {
|
|
779
|
+
// 50 lines of setup code...
|
|
780
|
+
database.connect();
|
|
781
|
+
seedTestData();
|
|
782
|
+
setupMocks();
|
|
783
|
+
configureEnvironment();
|
|
784
|
+
// ...
|
|
785
|
+
});
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
**✅ GOOD**: Extract to helper or fixture
|
|
789
|
+
```typescript
|
|
790
|
+
beforeEach(() => {
|
|
791
|
+
testEnv = createTestEnvironment(); // Encapsulated setup
|
|
792
|
+
});
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
### 3. The Giant (One test testing everything)
|
|
796
|
+
|
|
797
|
+
**❌ BAD**:
|
|
798
|
+
```typescript
|
|
799
|
+
it('should handle entire user lifecycle', () => {
|
|
800
|
+
// 200 lines of test code testing create, update, delete, search...
|
|
801
|
+
});
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
**✅ GOOD**: One test per behavior
|
|
805
|
+
```typescript
|
|
806
|
+
it('should create user', () => { /* ... */ });
|
|
807
|
+
it('should update user', () => { /* ... */ });
|
|
808
|
+
it('should delete user', () => { /* ... */ });
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
### 4. The Mockery (Over-mocking)
|
|
812
|
+
|
|
813
|
+
**❌ BAD**:
|
|
814
|
+
```typescript
|
|
815
|
+
vi.mock('./utils');
|
|
816
|
+
vi.mock('./helpers');
|
|
817
|
+
vi.mock('./validators');
|
|
818
|
+
vi.mock('./formatters');
|
|
819
|
+
// Testing nothing but mocks!
|
|
820
|
+
```
|
|
821
|
+
|
|
822
|
+
**✅ GOOD**: Mock only external dependencies
|
|
823
|
+
```typescript
|
|
824
|
+
vi.mock('./externalApi'); // Mock external API only
|
|
825
|
+
// Test real code integration
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### 5. The Slow Poke (Slow tests)
|
|
829
|
+
|
|
830
|
+
**❌ BAD**:
|
|
831
|
+
```typescript
|
|
832
|
+
it('should process data', async () => {
|
|
833
|
+
await sleep(5000); // Hardcoded delays
|
|
834
|
+
// ...
|
|
835
|
+
});
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
**✅ GOOD**: Use fake timers
|
|
839
|
+
```typescript
|
|
840
|
+
it('should process data', () => {
|
|
841
|
+
vi.useFakeTimers();
|
|
842
|
+
// ...
|
|
843
|
+
vi.advanceTimersByTime(5000);
|
|
844
|
+
vi.restoreAllTimers();
|
|
845
|
+
});
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
## TDD Metrics & Success Indicators
|
|
849
|
+
|
|
850
|
+
**Good TDD indicators**:
|
|
851
|
+
- ✅ 80%+ code coverage
|
|
852
|
+
- ✅ Tests run in < 1 second (unit tests)
|
|
853
|
+
- ✅ Tests are independent and can run in any order
|
|
854
|
+
- ✅ Tests are readable and self-documenting
|
|
855
|
+
- ✅ Red-Green-Refactor cycle followed consistently
|
|
856
|
+
- ✅ Code is modular and testable
|
|
857
|
+
|
|
858
|
+
**Warning signs**:
|
|
859
|
+
- ❌ Tests take minutes to run
|
|
860
|
+
- ❌ Tests fail randomly (flakiness)
|
|
861
|
+
- ❌ Hard to write tests (indicates design issues)
|
|
862
|
+
- ❌ Mocking everything (over-mocking)
|
|
863
|
+
- ❌ Tests break on refactoring (testing implementation)
|
|
864
|
+
|
|
865
|
+
## TDD in Practice: Real-World Tips
|
|
866
|
+
|
|
867
|
+
### 1. Start Small
|
|
868
|
+
Begin TDD on new features, not legacy code. Retrofit tests gradually.
|
|
869
|
+
|
|
870
|
+
### 2. Write Test First (Always!)
|
|
871
|
+
Resist urge to code first. The test is your design tool.
|
|
872
|
+
|
|
873
|
+
### 3. Keep Tests Fast
|
|
874
|
+
Unit tests should run in milliseconds. Slow tests kill TDD.
|
|
875
|
+
|
|
876
|
+
### 4. Commit Test + Code Together
|
|
877
|
+
Tests are part of the implementation, not afterthought.
|
|
878
|
+
|
|
879
|
+
### 5. Refactor Relentlessly
|
|
880
|
+
Green doesn't mean done. Refactor for clarity.
|
|
881
|
+
|
|
882
|
+
### 6. Test Behavior, Not Implementation
|
|
883
|
+
Tests should survive refactoring.
|
|
884
|
+
|
|
885
|
+
### 7. Use TDD to Drive Design
|
|
886
|
+
Let tests guide your architecture (dependency injection, SOLID).
|
|
887
|
+
|
|
888
|
+
## TDD Workflow Commands
|
|
889
|
+
|
|
890
|
+
**Development cycle**:
|
|
891
|
+
```bash
|
|
892
|
+
# 1. Write failing test (RED)
|
|
893
|
+
npm test -- --watch
|
|
894
|
+
|
|
895
|
+
# 2. Implement (GREEN)
|
|
896
|
+
# Make changes, watch tests turn green
|
|
897
|
+
|
|
898
|
+
# 3. Refactor
|
|
899
|
+
# Improve code, tests stay green
|
|
900
|
+
|
|
901
|
+
# 4. Commit
|
|
902
|
+
git add .
|
|
903
|
+
git commit -m "feat: add user validation"
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
**Coverage check**:
|
|
907
|
+
```bash
|
|
908
|
+
npm test -- --coverage
|
|
909
|
+
# Ensure 80%+ coverage
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
## Resources
|
|
913
|
+
|
|
914
|
+
- **Kent Beck**: "Test-Driven Development by Example"
|
|
915
|
+
- **Robert C. Martin**: "Clean Code" (TDD chapter)
|
|
916
|
+
- **Martin Fowler**: Refactoring patterns
|
|
917
|
+
- **Growing Object-Oriented Software, Guided by Tests** (Freeman & Pryce)
|
|
918
|
+
|
|
919
|
+
## Summary
|
|
920
|
+
|
|
921
|
+
**TDD Core Rules**:
|
|
922
|
+
1. Write test FIRST (red)
|
|
923
|
+
2. Make it pass MINIMALLY (green)
|
|
924
|
+
3. Refactor RELENTLESSLY (refactor)
|
|
925
|
+
4. Repeat
|
|
926
|
+
|
|
927
|
+
**Benefits**:
|
|
928
|
+
- Better design (testable = modular)
|
|
929
|
+
- Living documentation
|
|
930
|
+
- Fearless refactoring
|
|
931
|
+
- Fast feedback loop
|
|
932
|
+
- Higher confidence
|
|
933
|
+
|
|
934
|
+
**Remember**: TDD is about **design**, not just testing. Tests are a tool to drive better architecture.
|