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,1011 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: unit-testing-expert
|
|
3
|
+
description: Comprehensive unit testing expertise covering Vitest, Jest, test-driven development (TDD), mocking strategies, test coverage, snapshot testing, test architecture, testing patterns, dependency injection, test doubles (mocks, stubs, spies, fakes), async testing, error handling tests, parametric testing, test organization, code coverage analysis, mutation testing, and production-grade unit testing best practices. Activates for unit testing, vitest, jest, test-driven development, TDD, red-green-refactor, mocking, stubbing, spying, test doubles, test coverage, snapshot testing, test architecture, dependency injection, async testing, test patterns, code coverage, mutation testing, test isolation, test fixtures, AAA pattern, given-when-then, test organization, testing best practices, vi.fn, vi.mock, vi.spyOn, describe, it, expect, beforeEach, afterEach.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Unit Testing Expert
|
|
7
|
+
|
|
8
|
+
## Core Expertise
|
|
9
|
+
|
|
10
|
+
### 1. Vitest Fundamentals
|
|
11
|
+
**Modern Testing Framework** (Vite-native, Jest-compatible)
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
15
|
+
import { UserService } from './UserService';
|
|
16
|
+
|
|
17
|
+
describe('UserService', () => {
|
|
18
|
+
let userService: UserService;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
userService = new UserService();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should create a new user', () => {
|
|
29
|
+
const user = userService.create({ name: 'John', email: 'john@example.com' });
|
|
30
|
+
|
|
31
|
+
expect(user).toMatchObject({
|
|
32
|
+
id: expect.any(String),
|
|
33
|
+
name: 'John',
|
|
34
|
+
email: 'john@example.com',
|
|
35
|
+
createdAt: expect.any(Date),
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should throw error for invalid email', () => {
|
|
40
|
+
expect(() => {
|
|
41
|
+
userService.create({ name: 'John', email: 'invalid' });
|
|
42
|
+
}).toThrow('Invalid email format');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Test-Driven Development (TDD)
|
|
48
|
+
**Red-Green-Refactor Cycle**
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// RED: Write failing test first
|
|
52
|
+
describe('Calculator', () => {
|
|
53
|
+
it('should add two numbers', () => {
|
|
54
|
+
const calculator = new Calculator();
|
|
55
|
+
expect(calculator.add(2, 3)).toBe(5);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// GREEN: Implement minimal code to pass
|
|
60
|
+
class Calculator {
|
|
61
|
+
add(a: number, b: number): number {
|
|
62
|
+
return a + b;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// REFACTOR: Improve without breaking tests
|
|
67
|
+
class Calculator {
|
|
68
|
+
add(...numbers: number[]): number {
|
|
69
|
+
return numbers.reduce((sum, num) => sum + num, 0);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Verify tests still pass
|
|
74
|
+
it('should add multiple numbers', () => {
|
|
75
|
+
const calculator = new Calculator();
|
|
76
|
+
expect(calculator.add(1, 2, 3, 4)).toBe(10);
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**TDD Benefits**:
|
|
81
|
+
- Forces modular, testable design
|
|
82
|
+
- Prevents over-engineering
|
|
83
|
+
- Living documentation
|
|
84
|
+
- Confident refactoring
|
|
85
|
+
- Faster debugging
|
|
86
|
+
|
|
87
|
+
### 3. AAA Pattern (Arrange-Act-Assert)
|
|
88
|
+
**Structure for Clear Tests**
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
describe('OrderService', () => {
|
|
92
|
+
it('should calculate total with discount', () => {
|
|
93
|
+
// ARRANGE: Set up test data and dependencies
|
|
94
|
+
const orderService = new OrderService();
|
|
95
|
+
const order = {
|
|
96
|
+
items: [
|
|
97
|
+
{ price: 100, quantity: 2 },
|
|
98
|
+
{ price: 50, quantity: 1 },
|
|
99
|
+
],
|
|
100
|
+
discountCode: 'SAVE20',
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// ACT: Execute the behavior under test
|
|
104
|
+
const total = orderService.calculateTotal(order);
|
|
105
|
+
|
|
106
|
+
// ASSERT: Verify the result
|
|
107
|
+
expect(total).toBe(200); // (100*2 + 50*1) * 0.8 = 200
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Alternative: Given-When-Then (BDD Style)**
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
describe('OrderService', () => {
|
|
116
|
+
it('should apply discount when valid code is provided', () => {
|
|
117
|
+
// GIVEN: An order with items and a discount code
|
|
118
|
+
const orderService = new OrderService();
|
|
119
|
+
const order = createOrder({ discountCode: 'SAVE20' });
|
|
120
|
+
|
|
121
|
+
// WHEN: Calculating the total
|
|
122
|
+
const total = orderService.calculateTotal(order);
|
|
123
|
+
|
|
124
|
+
// THEN: The discount should be applied
|
|
125
|
+
expect(total).toBe(200);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### 4. Mocking Strategies
|
|
131
|
+
**Test Doubles: Mocks, Stubs, Spies, Fakes**
|
|
132
|
+
|
|
133
|
+
#### Mocks (Track Calls + Control Behavior)
|
|
134
|
+
```typescript
|
|
135
|
+
import { vi } from 'vitest';
|
|
136
|
+
|
|
137
|
+
describe('EmailService', () => {
|
|
138
|
+
it('should send welcome email on user registration', async () => {
|
|
139
|
+
// Mock external email API
|
|
140
|
+
const mockSendEmail = vi.fn().mockResolvedValue({ success: true });
|
|
141
|
+
const emailService = new EmailService({ sendEmail: mockSendEmail });
|
|
142
|
+
|
|
143
|
+
await emailService.sendWelcomeEmail('user@example.com');
|
|
144
|
+
|
|
145
|
+
// Verify mock was called correctly
|
|
146
|
+
expect(mockSendEmail).toHaveBeenCalledTimes(1);
|
|
147
|
+
expect(mockSendEmail).toHaveBeenCalledWith({
|
|
148
|
+
to: 'user@example.com',
|
|
149
|
+
subject: 'Welcome!',
|
|
150
|
+
body: expect.stringContaining('Welcome to our platform'),
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### Spies (Track Calls on Real Methods)
|
|
157
|
+
```typescript
|
|
158
|
+
describe('Logger', () => {
|
|
159
|
+
it('should log errors to console', () => {
|
|
160
|
+
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
161
|
+
|
|
162
|
+
const logger = new Logger();
|
|
163
|
+
logger.error('Something went wrong');
|
|
164
|
+
|
|
165
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith('[ERROR]', 'Something went wrong');
|
|
166
|
+
|
|
167
|
+
consoleErrorSpy.mockRestore();
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Stubs (Return Predefined Values)
|
|
173
|
+
```typescript
|
|
174
|
+
describe('UserRepository', () => {
|
|
175
|
+
it('should fetch user by id', async () => {
|
|
176
|
+
// Stub database query
|
|
177
|
+
const dbStub = {
|
|
178
|
+
query: vi.fn().mockResolvedValue({
|
|
179
|
+
rows: [{ id: 1, name: 'John' }],
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const repo = new UserRepository(dbStub);
|
|
184
|
+
const user = await repo.findById(1);
|
|
185
|
+
|
|
186
|
+
expect(user).toEqual({ id: 1, name: 'John' });
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### Fakes (Working Implementations for Testing)
|
|
192
|
+
```typescript
|
|
193
|
+
// Fake in-memory database
|
|
194
|
+
class FakeDatabase {
|
|
195
|
+
private data: Map<string, any> = new Map();
|
|
196
|
+
|
|
197
|
+
async save(key: string, value: any): Promise<void> {
|
|
198
|
+
this.data.set(key, value);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async find(key: string): Promise<any> {
|
|
202
|
+
return this.data.get(key);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async delete(key: string): Promise<void> {
|
|
206
|
+
this.data.delete(key);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
describe('CacheService', () => {
|
|
211
|
+
it('should store and retrieve values', async () => {
|
|
212
|
+
const fakeDb = new FakeDatabase();
|
|
213
|
+
const cache = new CacheService(fakeDb);
|
|
214
|
+
|
|
215
|
+
await cache.set('key', 'value');
|
|
216
|
+
const result = await cache.get('key');
|
|
217
|
+
|
|
218
|
+
expect(result).toBe('value');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### 5. Module Mocking
|
|
224
|
+
**Mock Entire Modules**
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
// Mock external dependency
|
|
228
|
+
vi.mock('./database', () => ({
|
|
229
|
+
Database: vi.fn().mockImplementation(() => ({
|
|
230
|
+
connect: vi.fn().mockResolvedValue(true),
|
|
231
|
+
query: vi.fn().mockResolvedValue({ rows: [] }),
|
|
232
|
+
disconnect: vi.fn(),
|
|
233
|
+
})),
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
import { Database } from './database';
|
|
237
|
+
import { UserService } from './UserService';
|
|
238
|
+
|
|
239
|
+
describe('UserService', () => {
|
|
240
|
+
it('should connect to database on initialization', async () => {
|
|
241
|
+
const userService = new UserService();
|
|
242
|
+
await userService.init();
|
|
243
|
+
|
|
244
|
+
expect(Database).toHaveBeenCalledTimes(1);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Partial Module Mocking**
|
|
250
|
+
|
|
251
|
+
```typescript
|
|
252
|
+
vi.mock('./utils', async (importOriginal) => {
|
|
253
|
+
const actual = await importOriginal();
|
|
254
|
+
return {
|
|
255
|
+
...actual,
|
|
256
|
+
// Mock only specific functions
|
|
257
|
+
fetchData: vi.fn().mockResolvedValue({ data: 'mocked' }),
|
|
258
|
+
};
|
|
259
|
+
});
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**Auto-mocking with vi.hoisted**
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { vi } from 'vitest';
|
|
266
|
+
|
|
267
|
+
// Hoist mocks to top (before imports)
|
|
268
|
+
vi.hoisted(() => {
|
|
269
|
+
vi.mock('./config', () => ({
|
|
270
|
+
API_URL: 'https://test-api.example.com',
|
|
271
|
+
}));
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
import { API_URL } from './config';
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### 6. Async Testing
|
|
278
|
+
**Handle Promises, Timers, and Callbacks**
|
|
279
|
+
|
|
280
|
+
#### Testing Promises
|
|
281
|
+
```typescript
|
|
282
|
+
describe('AsyncService', () => {
|
|
283
|
+
it('should resolve with data', async () => {
|
|
284
|
+
const service = new AsyncService();
|
|
285
|
+
|
|
286
|
+
const result = await service.fetchData();
|
|
287
|
+
|
|
288
|
+
expect(result).toEqual({ id: 1, name: 'Test' });
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should reject with error', async () => {
|
|
292
|
+
const service = new AsyncService();
|
|
293
|
+
|
|
294
|
+
await expect(service.fetchInvalidData()).rejects.toThrow('Not found');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should handle multiple async operations', async () => {
|
|
298
|
+
const service = new AsyncService();
|
|
299
|
+
|
|
300
|
+
const [user, posts] = await Promise.all([
|
|
301
|
+
service.fetchUser(1),
|
|
302
|
+
service.fetchPosts(1),
|
|
303
|
+
]);
|
|
304
|
+
|
|
305
|
+
expect(user.id).toBe(1);
|
|
306
|
+
expect(posts.length).toBeGreaterThan(0);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
#### Testing Timers
|
|
312
|
+
```typescript
|
|
313
|
+
describe('DebounceService', () => {
|
|
314
|
+
beforeEach(() => {
|
|
315
|
+
vi.useFakeTimers();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
afterEach(() => {
|
|
319
|
+
vi.restoreAllTimers();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should debounce function calls', () => {
|
|
323
|
+
const callback = vi.fn();
|
|
324
|
+
const debounced = debounce(callback, 1000);
|
|
325
|
+
|
|
326
|
+
debounced();
|
|
327
|
+
debounced();
|
|
328
|
+
debounced();
|
|
329
|
+
|
|
330
|
+
expect(callback).not.toHaveBeenCalled();
|
|
331
|
+
|
|
332
|
+
// Fast-forward time
|
|
333
|
+
vi.advanceTimersByTime(1000);
|
|
334
|
+
|
|
335
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it('should cancel pending debounced calls', () => {
|
|
339
|
+
const callback = vi.fn();
|
|
340
|
+
const debounced = debounce(callback, 1000);
|
|
341
|
+
|
|
342
|
+
debounced();
|
|
343
|
+
debounced.cancel();
|
|
344
|
+
|
|
345
|
+
vi.advanceTimersByTime(1000);
|
|
346
|
+
|
|
347
|
+
expect(callback).not.toHaveBeenCalled();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
#### Testing Callbacks
|
|
353
|
+
```typescript
|
|
354
|
+
describe('EventEmitter', () => {
|
|
355
|
+
it('should execute callback on event', (done) => {
|
|
356
|
+
const emitter = new EventEmitter();
|
|
357
|
+
|
|
358
|
+
emitter.on('data', (data) => {
|
|
359
|
+
expect(data).toBe('test');
|
|
360
|
+
done(); // Signal async completion
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
emitter.emit('data', 'test');
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// Modern alternative: Promisify
|
|
367
|
+
it('should execute callback on event (promisified)', () => {
|
|
368
|
+
const emitter = new EventEmitter();
|
|
369
|
+
|
|
370
|
+
const promise = new Promise((resolve) => {
|
|
371
|
+
emitter.on('data', resolve);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
emitter.emit('data', 'test');
|
|
375
|
+
|
|
376
|
+
return expect(promise).resolves.toBe('test');
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
### 7. Parametric Testing (Table-Driven Tests)
|
|
382
|
+
**Test Multiple Cases Efficiently**
|
|
383
|
+
|
|
384
|
+
```typescript
|
|
385
|
+
describe.each([
|
|
386
|
+
{ input: 2, expected: 4 },
|
|
387
|
+
{ input: 3, expected: 9 },
|
|
388
|
+
{ input: 4, expected: 16 },
|
|
389
|
+
{ input: 5, expected: 25 },
|
|
390
|
+
])('square($input)', ({ input, expected }) => {
|
|
391
|
+
it(`should return ${expected}`, () => {
|
|
392
|
+
expect(square(input)).toBe(expected);
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Alternative syntax
|
|
397
|
+
it.each([
|
|
398
|
+
[1, 2, 3],
|
|
399
|
+
[2, 3, 5],
|
|
400
|
+
[3, 4, 7],
|
|
401
|
+
])('add(%i, %i) should equal %i', (a, b, expected) => {
|
|
402
|
+
expect(add(a, b)).toBe(expected);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
// Complex validation
|
|
406
|
+
describe.each([
|
|
407
|
+
{ email: 'user@example.com', valid: true },
|
|
408
|
+
{ email: 'invalid', valid: false },
|
|
409
|
+
{ email: 'missing@', valid: false },
|
|
410
|
+
{ email: '@domain.com', valid: false },
|
|
411
|
+
{ email: '', valid: false },
|
|
412
|
+
])('validateEmail($email)', ({ email, valid }) => {
|
|
413
|
+
it(`should return ${valid}`, () => {
|
|
414
|
+
expect(validateEmail(email)).toBe(valid);
|
|
415
|
+
});
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### 8. Snapshot Testing
|
|
420
|
+
**Capture and Compare Complex Outputs**
|
|
421
|
+
|
|
422
|
+
```typescript
|
|
423
|
+
describe('ComponentRenderer', () => {
|
|
424
|
+
it('should render user profile correctly', () => {
|
|
425
|
+
const user = { id: 1, name: 'John', email: 'john@example.com' };
|
|
426
|
+
const rendered = renderUserProfile(user);
|
|
427
|
+
|
|
428
|
+
expect(rendered).toMatchSnapshot();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it('should render empty state', () => {
|
|
432
|
+
const rendered = renderUserProfile(null);
|
|
433
|
+
|
|
434
|
+
expect(rendered).toMatchSnapshot();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Inline snapshots (better for small outputs)
|
|
438
|
+
it('should format date', () => {
|
|
439
|
+
const formatted = formatDate(new Date('2025-01-15'));
|
|
440
|
+
|
|
441
|
+
expect(formatted).toMatchInlineSnapshot('"January 15, 2025"');
|
|
442
|
+
});
|
|
443
|
+
});
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
**Update snapshots**: `npm test -- -u`
|
|
447
|
+
|
|
448
|
+
**Snapshot Best Practices**:
|
|
449
|
+
- Use for UI components, API responses, complex objects
|
|
450
|
+
- Keep snapshots small and focused
|
|
451
|
+
- Review snapshot diffs carefully in PRs
|
|
452
|
+
- Avoid snapshots for simple values (use `.toBe()` instead)
|
|
453
|
+
|
|
454
|
+
### 9. Error Handling Tests
|
|
455
|
+
**Verify Error Conditions**
|
|
456
|
+
|
|
457
|
+
```typescript
|
|
458
|
+
describe('ValidationService', () => {
|
|
459
|
+
it('should throw for invalid input', () => {
|
|
460
|
+
const validator = new ValidationService();
|
|
461
|
+
|
|
462
|
+
expect(() => {
|
|
463
|
+
validator.validate(null);
|
|
464
|
+
}).toThrow('Input is required');
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('should throw specific error type', () => {
|
|
468
|
+
const validator = new ValidationService();
|
|
469
|
+
|
|
470
|
+
expect(() => {
|
|
471
|
+
validator.validate({ age: -1 });
|
|
472
|
+
}).toThrow(ValidationError);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
it('should include error details', () => {
|
|
476
|
+
const validator = new ValidationService();
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
validator.validate({ age: 'invalid' });
|
|
480
|
+
fail('Expected error to be thrown');
|
|
481
|
+
} catch (error) {
|
|
482
|
+
expect(error).toBeInstanceOf(ValidationError);
|
|
483
|
+
expect(error.message).toBe('Age must be a number');
|
|
484
|
+
expect(error.field).toBe('age');
|
|
485
|
+
expect(error.code).toBe('INVALID_TYPE');
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
it('should handle async errors', async () => {
|
|
490
|
+
const service = new AsyncService();
|
|
491
|
+
|
|
492
|
+
await expect(async () => {
|
|
493
|
+
await service.fetchWithInvalidToken();
|
|
494
|
+
}).rejects.toThrow('Unauthorized');
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
### 10. Dependency Injection for Testability
|
|
500
|
+
**Design for Easy Testing**
|
|
501
|
+
|
|
502
|
+
```typescript
|
|
503
|
+
// ❌ BAD: Hard to test (tight coupling)
|
|
504
|
+
class UserService {
|
|
505
|
+
async getUser(id: string) {
|
|
506
|
+
const db = new Database(); // Hard-coded dependency
|
|
507
|
+
return db.query('SELECT * FROM users WHERE id = ?', [id]);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ✅ GOOD: Easy to test (dependency injection)
|
|
512
|
+
class UserService {
|
|
513
|
+
constructor(private db: Database) {}
|
|
514
|
+
|
|
515
|
+
async getUser(id: string) {
|
|
516
|
+
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Test with mock
|
|
521
|
+
describe('UserService', () => {
|
|
522
|
+
it('should fetch user by id', async () => {
|
|
523
|
+
const mockDb = {
|
|
524
|
+
query: vi.fn().mockResolvedValue({ id: '1', name: 'John' }),
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const service = new UserService(mockDb as any);
|
|
528
|
+
const user = await service.getUser('1');
|
|
529
|
+
|
|
530
|
+
expect(user).toEqual({ id: '1', name: 'John' });
|
|
531
|
+
expect(mockDb.query).toHaveBeenCalledWith(
|
|
532
|
+
'SELECT * FROM users WHERE id = ?',
|
|
533
|
+
['1']
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Factory Pattern for Dependencies**
|
|
540
|
+
|
|
541
|
+
```typescript
|
|
542
|
+
interface Dependencies {
|
|
543
|
+
logger?: Logger;
|
|
544
|
+
cache?: Cache;
|
|
545
|
+
db?: Database;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
class UserService {
|
|
549
|
+
private logger: Logger;
|
|
550
|
+
private cache: Cache;
|
|
551
|
+
private db: Database;
|
|
552
|
+
|
|
553
|
+
constructor(deps: Dependencies = {}) {
|
|
554
|
+
this.logger = deps.logger ?? new ConsoleLogger();
|
|
555
|
+
this.cache = deps.cache ?? new RedisCache();
|
|
556
|
+
this.db = deps.db ?? new PostgresDatabase();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// Production
|
|
561
|
+
const service = new UserService();
|
|
562
|
+
|
|
563
|
+
// Testing
|
|
564
|
+
const service = new UserService({
|
|
565
|
+
logger: silentLogger,
|
|
566
|
+
cache: inMemoryCache,
|
|
567
|
+
db: mockDatabase,
|
|
568
|
+
});
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
### 11. Test Coverage Analysis
|
|
572
|
+
**Measure and Improve Coverage**
|
|
573
|
+
|
|
574
|
+
```typescript
|
|
575
|
+
// vitest.config.ts
|
|
576
|
+
export default defineConfig({
|
|
577
|
+
test: {
|
|
578
|
+
coverage: {
|
|
579
|
+
provider: 'v8', // or 'istanbul'
|
|
580
|
+
reporter: ['text', 'json', 'html', 'lcov'],
|
|
581
|
+
include: ['src/**/*.ts'],
|
|
582
|
+
exclude: [
|
|
583
|
+
'src/**/*.test.ts',
|
|
584
|
+
'src/**/*.spec.ts',
|
|
585
|
+
'src/types/**',
|
|
586
|
+
'src/index.ts',
|
|
587
|
+
],
|
|
588
|
+
thresholds: {
|
|
589
|
+
statements: 80,
|
|
590
|
+
branches: 75,
|
|
591
|
+
functions: 80,
|
|
592
|
+
lines: 80,
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
});
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
**Run with coverage**: `npm test -- --coverage`
|
|
600
|
+
|
|
601
|
+
**Coverage Types**:
|
|
602
|
+
- **Statement**: % of code statements executed
|
|
603
|
+
- **Branch**: % of conditional branches tested
|
|
604
|
+
- **Function**: % of functions called
|
|
605
|
+
- **Line**: % of lines executed
|
|
606
|
+
|
|
607
|
+
**Coverage Best Practices**:
|
|
608
|
+
- Aim for 80%+ overall coverage
|
|
609
|
+
- 100% coverage ≠ bug-free (test quality matters)
|
|
610
|
+
- Focus on critical paths and edge cases
|
|
611
|
+
- Use coverage to find untested code
|
|
612
|
+
- Don't game the system (write meaningful tests)
|
|
613
|
+
|
|
614
|
+
### 12. Test Organization
|
|
615
|
+
**Structure for Maintainability**
|
|
616
|
+
|
|
617
|
+
```
|
|
618
|
+
src/
|
|
619
|
+
├── services/
|
|
620
|
+
│ ├── UserService.ts
|
|
621
|
+
│ ├── UserService.test.ts # Co-located tests
|
|
622
|
+
│ ├── OrderService.ts
|
|
623
|
+
│ └── OrderService.test.ts
|
|
624
|
+
├── utils/
|
|
625
|
+
│ ├── validation.ts
|
|
626
|
+
│ ├── validation.test.ts
|
|
627
|
+
│ ├── formatting.ts
|
|
628
|
+
│ └── formatting.test.ts
|
|
629
|
+
└── __tests__/ # Alternative: separate test dir
|
|
630
|
+
├── unit/
|
|
631
|
+
│ ├── services/
|
|
632
|
+
│ │ ├── UserService.test.ts
|
|
633
|
+
│ │ └── OrderService.test.ts
|
|
634
|
+
│ └── utils/
|
|
635
|
+
│ ├── validation.test.ts
|
|
636
|
+
│ └── formatting.test.ts
|
|
637
|
+
└── integration/
|
|
638
|
+
├── api.test.ts
|
|
639
|
+
└── database.test.ts
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
**Naming Conventions**:
|
|
643
|
+
- Test files: `*.test.ts` or `*.spec.ts`
|
|
644
|
+
- Test suites: `describe('ClassName')`
|
|
645
|
+
- Test cases: `it('should do something specific')`
|
|
646
|
+
- Helper files: `*.fixture.ts`, `*.mock.ts`
|
|
647
|
+
|
|
648
|
+
### 13. Test Fixtures & Helpers
|
|
649
|
+
**Reusable Test Data and Utilities**
|
|
650
|
+
|
|
651
|
+
```typescript
|
|
652
|
+
// fixtures/user.fixture.ts
|
|
653
|
+
export const createUser = (overrides = {}) => ({
|
|
654
|
+
id: '1',
|
|
655
|
+
name: 'John Doe',
|
|
656
|
+
email: 'john@example.com',
|
|
657
|
+
createdAt: new Date('2025-01-01'),
|
|
658
|
+
...overrides,
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
export const createUserList = (count = 3) =>
|
|
662
|
+
Array.from({ length: count }, (_, i) =>
|
|
663
|
+
createUser({ id: String(i + 1), name: `User ${i + 1}` })
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
// Usage in tests
|
|
667
|
+
import { createUser, createUserList } from '../fixtures/user.fixture';
|
|
668
|
+
|
|
669
|
+
describe('UserRepository', () => {
|
|
670
|
+
it('should save user', async () => {
|
|
671
|
+
const user = createUser({ name: 'Jane' });
|
|
672
|
+
await repo.save(user);
|
|
673
|
+
|
|
674
|
+
expect(await repo.findById(user.id)).toEqual(user);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('should find all users', async () => {
|
|
678
|
+
const users = createUserList(5);
|
|
679
|
+
await Promise.all(users.map(u => repo.save(u)));
|
|
680
|
+
|
|
681
|
+
expect(await repo.findAll()).toHaveLength(5);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
**Test Helpers**
|
|
687
|
+
|
|
688
|
+
```typescript
|
|
689
|
+
// helpers/test-utils.ts
|
|
690
|
+
export const waitFor = (condition: () => boolean, timeout = 1000) => {
|
|
691
|
+
return new Promise((resolve, reject) => {
|
|
692
|
+
const startTime = Date.now();
|
|
693
|
+
const interval = setInterval(() => {
|
|
694
|
+
if (condition()) {
|
|
695
|
+
clearInterval(interval);
|
|
696
|
+
resolve(true);
|
|
697
|
+
} else if (Date.now() - startTime > timeout) {
|
|
698
|
+
clearInterval(interval);
|
|
699
|
+
reject(new Error('Timeout waiting for condition'));
|
|
700
|
+
}
|
|
701
|
+
}, 10);
|
|
702
|
+
});
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
export const flushPromises = () => new Promise(resolve => setImmediate(resolve));
|
|
706
|
+
|
|
707
|
+
export const createMockLogger = () => ({
|
|
708
|
+
info: vi.fn(),
|
|
709
|
+
warn: vi.fn(),
|
|
710
|
+
error: vi.fn(),
|
|
711
|
+
debug: vi.fn(),
|
|
712
|
+
});
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
### 14. Advanced Matchers
|
|
716
|
+
**Custom and Built-in Matchers**
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
describe('Advanced Matchers', () => {
|
|
720
|
+
// Equality
|
|
721
|
+
it('exact equality', () => expect(1 + 1).toBe(2));
|
|
722
|
+
it('deep equality', () => expect({ a: 1 }).toEqual({ a: 1 }));
|
|
723
|
+
it('reference equality', () => {
|
|
724
|
+
const obj = { a: 1 };
|
|
725
|
+
expect(obj).toBe(obj);
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
// Truthiness
|
|
729
|
+
it('truthy', () => expect(true).toBeTruthy());
|
|
730
|
+
it('falsy', () => expect(false).toBeFalsy());
|
|
731
|
+
it('defined', () => expect('value').toBeDefined());
|
|
732
|
+
it('undefined', () => expect(undefined).toBeUndefined());
|
|
733
|
+
it('null', () => expect(null).toBeNull());
|
|
734
|
+
|
|
735
|
+
// Numbers
|
|
736
|
+
it('greater than', () => expect(10).toBeGreaterThan(5));
|
|
737
|
+
it('less than', () => expect(5).toBeLessThan(10));
|
|
738
|
+
it('close to', () => expect(0.1 + 0.2).toBeCloseTo(0.3, 5));
|
|
739
|
+
|
|
740
|
+
// Strings
|
|
741
|
+
it('contains', () => expect('hello world').toContain('world'));
|
|
742
|
+
it('matches regex', () => expect('test@example.com').toMatch(/^\S+@\S+$/));
|
|
743
|
+
|
|
744
|
+
// Arrays
|
|
745
|
+
it('contains item', () => expect([1, 2, 3]).toContain(2));
|
|
746
|
+
it('has length', () => expect([1, 2, 3]).toHaveLength(3));
|
|
747
|
+
it('contains object', () => {
|
|
748
|
+
expect([{ id: 1 }, { id: 2 }]).toContainEqual({ id: 1 });
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
// Objects
|
|
752
|
+
it('matches object', () => {
|
|
753
|
+
expect({ id: 1, name: 'John', age: 30 }).toMatchObject({
|
|
754
|
+
id: 1,
|
|
755
|
+
name: 'John',
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
it('has property', () => expect({ a: 1 }).toHaveProperty('a'));
|
|
759
|
+
it('has property with value', () => expect({ a: 1 }).toHaveProperty('a', 1));
|
|
760
|
+
|
|
761
|
+
// Functions
|
|
762
|
+
it('throws', () => {
|
|
763
|
+
expect(() => { throw new Error('fail'); }).toThrow('fail');
|
|
764
|
+
});
|
|
765
|
+
it('called', () => {
|
|
766
|
+
const mock = vi.fn();
|
|
767
|
+
mock();
|
|
768
|
+
expect(mock).toHaveBeenCalled();
|
|
769
|
+
});
|
|
770
|
+
it('called with', () => {
|
|
771
|
+
const mock = vi.fn();
|
|
772
|
+
mock(1, 2);
|
|
773
|
+
expect(mock).toHaveBeenCalledWith(1, 2);
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// Negation
|
|
777
|
+
it('not equal', () => expect(1).not.toBe(2));
|
|
778
|
+
});
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
**Custom Matchers**
|
|
782
|
+
|
|
783
|
+
```typescript
|
|
784
|
+
import { expect } from 'vitest';
|
|
785
|
+
|
|
786
|
+
expect.extend({
|
|
787
|
+
toBeValidEmail(received: string) {
|
|
788
|
+
const pass = /^\S+@\S+\.\S+$/.test(received);
|
|
789
|
+
return {
|
|
790
|
+
pass,
|
|
791
|
+
message: () =>
|
|
792
|
+
pass
|
|
793
|
+
? `expected ${received} not to be a valid email`
|
|
794
|
+
: `expected ${received} to be a valid email`,
|
|
795
|
+
};
|
|
796
|
+
},
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// Usage
|
|
800
|
+
it('should validate email', () => {
|
|
801
|
+
expect('user@example.com').toBeValidEmail();
|
|
802
|
+
expect('invalid').not.toBeValidEmail();
|
|
803
|
+
});
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
### 15. Test Lifecycle Hooks
|
|
807
|
+
**Setup and Teardown**
|
|
808
|
+
|
|
809
|
+
```typescript
|
|
810
|
+
describe('Database Tests', () => {
|
|
811
|
+
let db: Database;
|
|
812
|
+
|
|
813
|
+
// Run once before all tests in suite
|
|
814
|
+
beforeAll(async () => {
|
|
815
|
+
db = new Database();
|
|
816
|
+
await db.connect();
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
// Run once after all tests in suite
|
|
820
|
+
afterAll(async () => {
|
|
821
|
+
await db.disconnect();
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// Run before each test
|
|
825
|
+
beforeEach(async () => {
|
|
826
|
+
await db.clear();
|
|
827
|
+
await db.seed();
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// Run after each test
|
|
831
|
+
afterEach(async () => {
|
|
832
|
+
await db.rollback();
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
it('should insert user', async () => {
|
|
836
|
+
await db.insert('users', { name: 'John' });
|
|
837
|
+
const users = await db.query('users');
|
|
838
|
+
expect(users).toHaveLength(1);
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
it('should delete user', async () => {
|
|
842
|
+
await db.insert('users', { name: 'John' });
|
|
843
|
+
await db.delete('users', { name: 'John' });
|
|
844
|
+
const users = await db.query('users');
|
|
845
|
+
expect(users).toHaveLength(0);
|
|
846
|
+
});
|
|
847
|
+
});
|
|
848
|
+
```
|
|
849
|
+
|
|
850
|
+
**Conditional Execution**
|
|
851
|
+
|
|
852
|
+
```typescript
|
|
853
|
+
// Skip tests
|
|
854
|
+
it.skip('not ready yet', () => {});
|
|
855
|
+
it.todo('implement later');
|
|
856
|
+
|
|
857
|
+
// Only run specific tests
|
|
858
|
+
it.only('focus on this test', () => {});
|
|
859
|
+
|
|
860
|
+
// Run if condition met
|
|
861
|
+
it.runIf(process.env.CI)('CI only test', () => {});
|
|
862
|
+
|
|
863
|
+
// Skip if condition met
|
|
864
|
+
it.skipIf(process.platform === 'win32')('Unix only test', () => {});
|
|
865
|
+
|
|
866
|
+
// Concurrent execution
|
|
867
|
+
describe.concurrent('parallel tests', () => {
|
|
868
|
+
it('test 1', async () => { /* runs in parallel */ });
|
|
869
|
+
it('test 2', async () => { /* runs in parallel */ });
|
|
870
|
+
});
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
## Best Practices
|
|
874
|
+
|
|
875
|
+
### Test Isolation
|
|
876
|
+
- Each test should be independent
|
|
877
|
+
- No shared state between tests
|
|
878
|
+
- Use `beforeEach` to reset state
|
|
879
|
+
- Avoid global variables
|
|
880
|
+
- Clean up resources in `afterEach`
|
|
881
|
+
|
|
882
|
+
### Test Naming
|
|
883
|
+
- Use descriptive names: `it('should return user when id exists')`
|
|
884
|
+
- Follow "should" convention
|
|
885
|
+
- Be specific about what is being tested
|
|
886
|
+
- Include edge cases in name: `it('should handle empty array')`
|
|
887
|
+
|
|
888
|
+
### Avoid Test Smells
|
|
889
|
+
❌ **Don't**:
|
|
890
|
+
- Test implementation details
|
|
891
|
+
- Write slow tests (mock external deps)
|
|
892
|
+
- Use magic numbers (use constants)
|
|
893
|
+
- Share state between tests
|
|
894
|
+
- Test framework code (test YOUR code)
|
|
895
|
+
|
|
896
|
+
✅ **Do**:
|
|
897
|
+
- Test behavior, not implementation
|
|
898
|
+
- Keep tests fast (< 100ms per test)
|
|
899
|
+
- Use descriptive variable names
|
|
900
|
+
- Isolate tests completely
|
|
901
|
+
- Focus on edge cases and error paths
|
|
902
|
+
|
|
903
|
+
### Performance
|
|
904
|
+
- Mock expensive operations (DB, API, file I/O)
|
|
905
|
+
- Use fake timers for time-based code
|
|
906
|
+
- Run tests in parallel (`--threads`)
|
|
907
|
+
- Cache test fixtures
|
|
908
|
+
- Profile slow tests: `npm test -- --reporter=verbose`
|
|
909
|
+
|
|
910
|
+
## Common Patterns
|
|
911
|
+
|
|
912
|
+
### Testing Classes
|
|
913
|
+
```typescript
|
|
914
|
+
class Counter {
|
|
915
|
+
private count = 0;
|
|
916
|
+
|
|
917
|
+
increment() { this.count++; }
|
|
918
|
+
decrement() { this.count--; }
|
|
919
|
+
getValue() { return this.count; }
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
describe('Counter', () => {
|
|
923
|
+
let counter: Counter;
|
|
924
|
+
|
|
925
|
+
beforeEach(() => {
|
|
926
|
+
counter = new Counter();
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
it('should start at 0', () => {
|
|
930
|
+
expect(counter.getValue()).toBe(0);
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
it('should increment', () => {
|
|
934
|
+
counter.increment();
|
|
935
|
+
expect(counter.getValue()).toBe(1);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
it('should decrement', () => {
|
|
939
|
+
counter.decrement();
|
|
940
|
+
expect(counter.getValue()).toBe(-1);
|
|
941
|
+
});
|
|
942
|
+
});
|
|
943
|
+
```
|
|
944
|
+
|
|
945
|
+
### Testing Utilities
|
|
946
|
+
```typescript
|
|
947
|
+
describe('formatCurrency', () => {
|
|
948
|
+
it.each([
|
|
949
|
+
[1000, '$1,000.00'],
|
|
950
|
+
[0.5, '$0.50'],
|
|
951
|
+
[-100, '-$100.00'],
|
|
952
|
+
])('formatCurrency(%i) should return %s', (input, expected) => {
|
|
953
|
+
expect(formatCurrency(input)).toBe(expected);
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
### Testing Hooks (React, Vue)
|
|
959
|
+
```typescript
|
|
960
|
+
import { renderHook, waitFor } from '@testing-library/react';
|
|
961
|
+
import { useCounter } from './useCounter';
|
|
962
|
+
|
|
963
|
+
describe('useCounter', () => {
|
|
964
|
+
it('should increment', () => {
|
|
965
|
+
const { result } = renderHook(() => useCounter());
|
|
966
|
+
|
|
967
|
+
act(() => {
|
|
968
|
+
result.current.increment();
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
expect(result.current.count).toBe(1);
|
|
972
|
+
});
|
|
973
|
+
});
|
|
974
|
+
```
|
|
975
|
+
|
|
976
|
+
## Troubleshooting
|
|
977
|
+
|
|
978
|
+
### Common Issues
|
|
979
|
+
1. **Timeouts**: Increase timeout for slow async operations
|
|
980
|
+
2. **Flaky tests**: Ensure proper cleanup, avoid race conditions
|
|
981
|
+
3. **Mock not working**: Check mock is hoisted, correct path
|
|
982
|
+
4. **Coverage gaps**: Use `--coverage` to identify untested code
|
|
983
|
+
5. **Slow tests**: Profile and mock expensive operations
|
|
984
|
+
|
|
985
|
+
### Debug Strategies
|
|
986
|
+
```bash
|
|
987
|
+
# Run single test
|
|
988
|
+
npm test -- path/to/test.ts
|
|
989
|
+
|
|
990
|
+
# Run tests matching pattern
|
|
991
|
+
npm test -- --grep "UserService"
|
|
992
|
+
|
|
993
|
+
# Debug mode (Node inspector)
|
|
994
|
+
node --inspect-brk node_modules/.bin/vitest run
|
|
995
|
+
|
|
996
|
+
# Watch mode
|
|
997
|
+
npm test -- --watch
|
|
998
|
+
|
|
999
|
+
# Verbose output
|
|
1000
|
+
npm test -- --reporter=verbose
|
|
1001
|
+
|
|
1002
|
+
# Coverage report
|
|
1003
|
+
npm test -- --coverage
|
|
1004
|
+
```
|
|
1005
|
+
|
|
1006
|
+
## Resources
|
|
1007
|
+
- **Vitest Docs**: https://vitest.dev
|
|
1008
|
+
- **Testing Library**: https://testing-library.com
|
|
1009
|
+
- **Jest API**: https://jestjs.io/docs/api (Jest-compatible)
|
|
1010
|
+
- **TDD Guide**: https://martinfowler.com/bliki/TestDrivenDevelopment.html
|
|
1011
|
+
- **Test Doubles**: https://martinfowler.com/bliki/TestDouble.html
|