@northbridge-security/secureai 0.1.13
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/README.md +122 -0
- package/.claude/commands/architect/clean.md +978 -0
- package/.claude/commands/architect/kiss.md +762 -0
- package/.claude/commands/architect/review.md +704 -0
- package/.claude/commands/catchup.md +90 -0
- package/.claude/commands/code.md +115 -0
- package/.claude/commands/commit.md +1218 -0
- package/.claude/commands/cover.md +1298 -0
- package/.claude/commands/fmea.md +275 -0
- package/.claude/commands/kaizen.md +312 -0
- package/.claude/commands/pr.md +503 -0
- package/.claude/commands/todo.md +99 -0
- package/.claude/commands/worktree.md +738 -0
- package/.claude/commands/wrapup.md +103 -0
- package/LICENSE +183 -0
- package/README.md +108 -0
- package/dist/cli.js +75634 -0
- package/docs/agents/devops-reviewer.md +889 -0
- package/docs/agents/kiss-simplifier.md +1088 -0
- package/docs/agents/typescript.md +8 -0
- package/docs/guides/README.md +109 -0
- package/docs/guides/agents.clean.arch.md +244 -0
- package/docs/guides/agents.clean.arch.ts.md +1314 -0
- package/docs/guides/agents.gotask.md +1037 -0
- package/docs/guides/agents.markdown.md +1209 -0
- package/docs/guides/agents.onepassword.md +285 -0
- package/docs/guides/agents.sonar.md +857 -0
- package/docs/guides/agents.tdd.md +838 -0
- package/docs/guides/agents.tdd.ts.md +1062 -0
- package/docs/guides/agents.typesript.md +1389 -0
- package/docs/guides/github-mcp.md +1075 -0
- package/package.json +130 -0
- package/packages/secureai-cli/src/cli.ts +21 -0
- package/tasks/README.md +880 -0
- package/tasks/aws.yml +64 -0
- package/tasks/bash.yml +118 -0
- package/tasks/bun.yml +738 -0
- package/tasks/claude.yml +183 -0
- package/tasks/docker.yml +420 -0
- package/tasks/docs.yml +127 -0
- package/tasks/git.yml +1336 -0
- package/tasks/gotask.yml +132 -0
- package/tasks/json.yml +77 -0
- package/tasks/markdown.yml +95 -0
- package/tasks/onepassword.yml +350 -0
- package/tasks/security.yml +102 -0
- package/tasks/sonar.yml +437 -0
- package/tasks/template.yml +74 -0
- package/tasks/vscode.yml +103 -0
- package/tasks/yaml.yml +121 -0
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
# Test-Driven Development in TypeScript
|
|
2
|
+
|
|
3
|
+
This guide provides TypeScript-specific TDD implementation details for AI agents. It covers Bun test framework, mocking patterns, and TypeScript best practices.
|
|
4
|
+
|
|
5
|
+
## Target Audience
|
|
6
|
+
|
|
7
|
+
AI agents writing TypeScript code with Bun test framework (similar to Jest). Applicable to Node.js, Bun, and Deno runtimes.
|
|
8
|
+
|
|
9
|
+
## Prerequisites
|
|
10
|
+
|
|
11
|
+
**Required knowledge:**
|
|
12
|
+
|
|
13
|
+
- Basic TDD principles (see [agents.tdd.md](./agents.tdd.md))
|
|
14
|
+
- TypeScript syntax and type system
|
|
15
|
+
- Async/await patterns
|
|
16
|
+
- Node.js/Bun APIs
|
|
17
|
+
|
|
18
|
+
**Setup:**
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Install bun (if not installed)
|
|
22
|
+
curl -fsSL https://bun.sh/install | bash
|
|
23
|
+
|
|
24
|
+
# Verify installation
|
|
25
|
+
bun --version
|
|
26
|
+
|
|
27
|
+
# Install project dependencies
|
|
28
|
+
bun install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Test Framework: Bun Test
|
|
32
|
+
|
|
33
|
+
### Basic Structure
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
/**
|
|
37
|
+
* Copyright (c) 2024-2025 Your Organization
|
|
38
|
+
* All rights reserved.
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
42
|
+
import { functionToTest } from "@src/module";
|
|
43
|
+
|
|
44
|
+
describe("Module Name", () => {
|
|
45
|
+
// Setup runs before each test
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
// Initialize test state
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Cleanup runs after each test
|
|
51
|
+
afterEach(() => {
|
|
52
|
+
// Clean up resources
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should perform expected behavior", () => {
|
|
56
|
+
// Arrange
|
|
57
|
+
const input = "test";
|
|
58
|
+
|
|
59
|
+
// Act
|
|
60
|
+
const result = functionToTest(input);
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(result).toBe("expected");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Import Paths
|
|
69
|
+
|
|
70
|
+
**Use TypeScript path aliases:**
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// GOOD - Path alias (configured in tsconfig.json)
|
|
74
|
+
import { utility } from "@src/utils/utility";
|
|
75
|
+
import { Config } from "@src/types";
|
|
76
|
+
|
|
77
|
+
// BAD - Relative paths
|
|
78
|
+
import { utility } from "../../../src/utils/utility";
|
|
79
|
+
import { Config } from "../../../src/types";
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**Path mapping in `tsconfig.json`:**
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{
|
|
86
|
+
"compilerOptions": {
|
|
87
|
+
"paths": {
|
|
88
|
+
"@src/*": ["./src/*"],
|
|
89
|
+
"@tests/*": ["./tests/*"]
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Test Discovery
|
|
96
|
+
|
|
97
|
+
**Bun automatically finds tests:**
|
|
98
|
+
|
|
99
|
+
- Files matching `*.test.ts` or `*.test.tsx`
|
|
100
|
+
- Files in `tests/` directory
|
|
101
|
+
- `__tests__/` directories
|
|
102
|
+
|
|
103
|
+
**Run tests:**
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# Run all tests
|
|
107
|
+
bun test
|
|
108
|
+
|
|
109
|
+
# Run specific file
|
|
110
|
+
bun test tests/unit/module.test.ts
|
|
111
|
+
|
|
112
|
+
# Run tests matching pattern
|
|
113
|
+
bun test --filter="email validation"
|
|
114
|
+
|
|
115
|
+
# Run with coverage
|
|
116
|
+
bun test --coverage
|
|
117
|
+
|
|
118
|
+
# Watch mode
|
|
119
|
+
bun test --watch
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## Assertions
|
|
123
|
+
|
|
124
|
+
### Basic Assertions
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { expect } from "bun:test";
|
|
128
|
+
|
|
129
|
+
// Equality
|
|
130
|
+
expect(actual).toBe(expected); // Strict equality (===)
|
|
131
|
+
expect(actual).toEqual(expected); // Deep equality
|
|
132
|
+
expect(actual).not.toBe(unexpected);
|
|
133
|
+
|
|
134
|
+
// Truthiness
|
|
135
|
+
expect(value).toBeTruthy();
|
|
136
|
+
expect(value).toBeFalsy();
|
|
137
|
+
expect(value).toBeDefined();
|
|
138
|
+
expect(value).toBeUndefined();
|
|
139
|
+
expect(value).toBeNull();
|
|
140
|
+
|
|
141
|
+
// Numbers
|
|
142
|
+
expect(num).toBeGreaterThan(5);
|
|
143
|
+
expect(num).toBeGreaterThanOrEqual(5);
|
|
144
|
+
expect(num).toBeLessThan(10);
|
|
145
|
+
expect(num).toBeLessThanOrEqual(10);
|
|
146
|
+
expect(float).toBeCloseTo(0.3, 5); // 5 decimal places
|
|
147
|
+
|
|
148
|
+
// Strings
|
|
149
|
+
expect(str).toMatch(/pattern/);
|
|
150
|
+
expect(str).toContain("substring");
|
|
151
|
+
expect(str).toHaveLength(10);
|
|
152
|
+
|
|
153
|
+
// Arrays
|
|
154
|
+
expect(arr).toContain("item");
|
|
155
|
+
expect(arr).toHaveLength(5);
|
|
156
|
+
expect(arr).toEqual(expect.arrayContaining(["a", "b"]));
|
|
157
|
+
|
|
158
|
+
// Objects
|
|
159
|
+
expect(obj).toHaveProperty("key");
|
|
160
|
+
expect(obj).toHaveProperty("key", "value");
|
|
161
|
+
expect(obj).toMatchObject({ key: "value" });
|
|
162
|
+
expect(obj).toEqual(expect.objectContaining({ key: "value" }));
|
|
163
|
+
|
|
164
|
+
// Instances
|
|
165
|
+
expect(obj).toBeInstanceOf(Date);
|
|
166
|
+
expect(obj).toBeInstanceOf(Error);
|
|
167
|
+
|
|
168
|
+
// Exceptions
|
|
169
|
+
expect(() => throwError()).toThrow();
|
|
170
|
+
expect(() => throwError()).toThrow("error message");
|
|
171
|
+
expect(() => throwError()).toThrow(TypeError);
|
|
172
|
+
|
|
173
|
+
// Promises
|
|
174
|
+
await expect(promise).resolves.toBe("value");
|
|
175
|
+
await expect(promise).rejects.toThrow("error");
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Custom Matchers
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// Extend expect with custom matchers
|
|
182
|
+
expect.extend({
|
|
183
|
+
toBeValidEmail(received: string) {
|
|
184
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
185
|
+
const pass = emailRegex.test(received);
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
pass,
|
|
189
|
+
message: () =>
|
|
190
|
+
pass
|
|
191
|
+
? `Expected ${received} not to be a valid email`
|
|
192
|
+
: `Expected ${received} to be a valid email`,
|
|
193
|
+
};
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Usage
|
|
198
|
+
expect("test@example.com").toBeValidEmail();
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Async Testing
|
|
202
|
+
|
|
203
|
+
### Testing Async Functions
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { describe, it, expect } from "bun:test";
|
|
207
|
+
|
|
208
|
+
describe("Async operations", () => {
|
|
209
|
+
it("should handle promises", async () => {
|
|
210
|
+
// Using await
|
|
211
|
+
const result = await fetchData();
|
|
212
|
+
expect(result).toBe("data");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("should handle promise resolution", async () => {
|
|
216
|
+
// Using resolves
|
|
217
|
+
await expect(fetchData()).resolves.toBe("data");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("should handle promise rejection", async () => {
|
|
221
|
+
// Using rejects
|
|
222
|
+
await expect(failingOperation()).rejects.toThrow("Error message");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("should handle multiple promises", async () => {
|
|
226
|
+
// Using Promise.all
|
|
227
|
+
const [result1, result2] = await Promise.all([fetchData(), fetchMoreData()]);
|
|
228
|
+
|
|
229
|
+
expect(result1).toBe("data1");
|
|
230
|
+
expect(result2).toBe("data2");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Timeouts
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { describe, it, expect } from "bun:test";
|
|
239
|
+
|
|
240
|
+
describe("Slow operations", () => {
|
|
241
|
+
it(
|
|
242
|
+
"should handle slow operation",
|
|
243
|
+
async () => {
|
|
244
|
+
const result = await slowOperation();
|
|
245
|
+
expect(result).toBeDefined();
|
|
246
|
+
},
|
|
247
|
+
{ timeout: 10000 } // 10 seconds
|
|
248
|
+
);
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Mocking
|
|
253
|
+
|
|
254
|
+
### Mock Functions
|
|
255
|
+
|
|
256
|
+
```typescript
|
|
257
|
+
import { describe, it, expect, mock } from "bun:test";
|
|
258
|
+
|
|
259
|
+
describe("Mock functions", () => {
|
|
260
|
+
it("should track function calls", () => {
|
|
261
|
+
// Create mock function
|
|
262
|
+
const mockFn = mock(() => "return value");
|
|
263
|
+
|
|
264
|
+
// Call mock
|
|
265
|
+
const result = mockFn("arg1", "arg2");
|
|
266
|
+
|
|
267
|
+
// Assert return value
|
|
268
|
+
expect(result).toBe("return value");
|
|
269
|
+
|
|
270
|
+
// Assert call count
|
|
271
|
+
expect(mockFn).toHaveBeenCalled();
|
|
272
|
+
expect(mockFn).toHaveBeenCalledTimes(1);
|
|
273
|
+
|
|
274
|
+
// Assert arguments
|
|
275
|
+
expect(mockFn).toHaveBeenCalledWith("arg1", "arg2");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it("should implement custom behavior", () => {
|
|
279
|
+
const mockFn = mock((x: number) => x * 2);
|
|
280
|
+
|
|
281
|
+
expect(mockFn(5)).toBe(10);
|
|
282
|
+
expect(mockFn(10)).toBe(20);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("should return different values on successive calls", () => {
|
|
286
|
+
const mockFn = mock();
|
|
287
|
+
|
|
288
|
+
mockFn.mockReturnValueOnce("first");
|
|
289
|
+
mockFn.mockReturnValueOnce("second");
|
|
290
|
+
mockFn.mockReturnValue("default");
|
|
291
|
+
|
|
292
|
+
expect(mockFn()).toBe("first");
|
|
293
|
+
expect(mockFn()).toBe("second");
|
|
294
|
+
expect(mockFn()).toBe("default");
|
|
295
|
+
expect(mockFn()).toBe("default");
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Spying on Functions
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { describe, it, expect, spyOn } from "bun:test";
|
|
304
|
+
|
|
305
|
+
describe("Function spies", () => {
|
|
306
|
+
it("should spy on object method", () => {
|
|
307
|
+
const obj = {
|
|
308
|
+
method: (x: number) => x * 2,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
// Create spy
|
|
312
|
+
const spy = spyOn(obj, "method");
|
|
313
|
+
|
|
314
|
+
// Call original method
|
|
315
|
+
const result = obj.method(5);
|
|
316
|
+
|
|
317
|
+
// Assert behavior
|
|
318
|
+
expect(result).toBe(10); // Original implementation
|
|
319
|
+
expect(spy).toHaveBeenCalledWith(5);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("should override implementation", () => {
|
|
323
|
+
const obj = {
|
|
324
|
+
method: (x: number) => x * 2,
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
// Spy with custom implementation
|
|
328
|
+
const spy = spyOn(obj, "method").mockImplementation((x: number) => x * 3);
|
|
329
|
+
|
|
330
|
+
expect(obj.method(5)).toBe(15); // Mock implementation
|
|
331
|
+
expect(spy).toHaveBeenCalled();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Module Mocking
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { describe, it, expect, mock } from "bun:test";
|
|
340
|
+
|
|
341
|
+
// Mock module at top level
|
|
342
|
+
mock.module("@src/database", () => ({
|
|
343
|
+
connect: mock(() => Promise.resolve()),
|
|
344
|
+
query: mock(() => Promise.resolve([])),
|
|
345
|
+
disconnect: mock(() => Promise.resolve()),
|
|
346
|
+
}));
|
|
347
|
+
|
|
348
|
+
describe("Database operations", () => {
|
|
349
|
+
it("should use mocked database", async () => {
|
|
350
|
+
const { connect, query } = await import("@src/database");
|
|
351
|
+
|
|
352
|
+
await connect();
|
|
353
|
+
const result = await query("SELECT * FROM users");
|
|
354
|
+
|
|
355
|
+
expect(connect).toHaveBeenCalled();
|
|
356
|
+
expect(query).toHaveBeenCalledWith("SELECT * FROM users");
|
|
357
|
+
expect(result).toEqual([]);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### Restoring Mocks
|
|
363
|
+
|
|
364
|
+
```typescript
|
|
365
|
+
import { describe, it, afterEach, mock } from "bun:test";
|
|
366
|
+
|
|
367
|
+
describe("Mock cleanup", () => {
|
|
368
|
+
afterEach(() => {
|
|
369
|
+
// Restore all mocks after each test
|
|
370
|
+
mock.restore();
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it("should use mock", () => {
|
|
374
|
+
const fn = mock(() => "mocked");
|
|
375
|
+
expect(fn()).toBe("mocked");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should have fresh mock", () => {
|
|
379
|
+
// Previous mock is restored
|
|
380
|
+
const fn = mock(() => "fresh");
|
|
381
|
+
expect(fn()).toBe("fresh");
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
## File System Testing
|
|
387
|
+
|
|
388
|
+
### Temporary Directories
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
392
|
+
import { mkdtempSync, rmSync, writeFileSync, realpathSync } from "node:fs";
|
|
393
|
+
import { tmpdir } from "node:os";
|
|
394
|
+
import { join } from "node:path";
|
|
395
|
+
|
|
396
|
+
describe("File operations", () => {
|
|
397
|
+
let testDir: string;
|
|
398
|
+
|
|
399
|
+
beforeEach(() => {
|
|
400
|
+
// Create temporary directory
|
|
401
|
+
// Use realpathSync to resolve symlinks on macOS (/var -> /private/var)
|
|
402
|
+
testDir = realpathSync(mkdtempSync(join(tmpdir(), "test-")));
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
afterEach(() => {
|
|
406
|
+
// Clean up temporary directory
|
|
407
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("should read file", () => {
|
|
411
|
+
// Arrange: Create test file
|
|
412
|
+
const testFile = join(testDir, "test.txt");
|
|
413
|
+
writeFileSync(testFile, "test content", "utf-8");
|
|
414
|
+
|
|
415
|
+
// Act: Read file
|
|
416
|
+
const content = readFile(testFile);
|
|
417
|
+
|
|
418
|
+
// Assert: Verify content
|
|
419
|
+
expect(content).toBe("test content");
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Working Directory Isolation
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
import { describe, it, beforeEach, afterEach } from "bun:test";
|
|
428
|
+
|
|
429
|
+
// Use serial execution to avoid race conditions
|
|
430
|
+
describe.serial("Directory operations", () => {
|
|
431
|
+
let originalCwd: string;
|
|
432
|
+
|
|
433
|
+
beforeEach(() => {
|
|
434
|
+
originalCwd = process.cwd();
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
afterEach(() => {
|
|
438
|
+
// Restore original working directory
|
|
439
|
+
process.chdir(originalCwd);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it("should change directory", () => {
|
|
443
|
+
const newDir = "/tmp";
|
|
444
|
+
process.chdir(newDir);
|
|
445
|
+
|
|
446
|
+
expect(process.cwd()).toBe(newDir);
|
|
447
|
+
});
|
|
448
|
+
});
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
## Platform-Specific Testing
|
|
452
|
+
|
|
453
|
+
### Conditional Test Execution
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { describe, it, expect } from "bun:test";
|
|
457
|
+
import { platform } from "node:os";
|
|
458
|
+
|
|
459
|
+
const isMacOS = platform() === "darwin";
|
|
460
|
+
const isLinux = platform() === "linux";
|
|
461
|
+
const isWindows = platform() === "win32";
|
|
462
|
+
|
|
463
|
+
describe("Platform-specific features", () => {
|
|
464
|
+
it.skipIf(!isMacOS)("should run on macOS only", () => {
|
|
465
|
+
// Test macOS-specific behavior
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
it.skipIf(!isLinux)("should run on Linux only", () => {
|
|
469
|
+
// Test Linux-specific behavior
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
it.skipIf(!isWindows)("should run on Windows only", () => {
|
|
473
|
+
// Test Windows-specific behavior
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it.skipIf(isMacOS)("should skip on macOS", () => {
|
|
477
|
+
// Test runs on Linux and Windows
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### Environment Variables
|
|
483
|
+
|
|
484
|
+
```typescript
|
|
485
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
486
|
+
|
|
487
|
+
describe("Environment configuration", () => {
|
|
488
|
+
let originalEnv: NodeJS.ProcessEnv;
|
|
489
|
+
|
|
490
|
+
beforeEach(() => {
|
|
491
|
+
// Save original environment
|
|
492
|
+
originalEnv = { ...process.env };
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
afterEach(() => {
|
|
496
|
+
// Restore original environment
|
|
497
|
+
process.env = originalEnv;
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("should use environment variable", () => {
|
|
501
|
+
// Set test environment variable
|
|
502
|
+
process.env.API_KEY = "test-key";
|
|
503
|
+
|
|
504
|
+
// Test code that uses process.env.API_KEY
|
|
505
|
+
expect(getApiKey()).toBe("test-key");
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
## Test Organization Patterns
|
|
511
|
+
|
|
512
|
+
### Test Factories
|
|
513
|
+
|
|
514
|
+
**Create reusable test data:**
|
|
515
|
+
|
|
516
|
+
```typescript
|
|
517
|
+
// tests/factories/user.ts
|
|
518
|
+
export function createTestUser(overrides?: Partial<User>): User {
|
|
519
|
+
return {
|
|
520
|
+
id: "123",
|
|
521
|
+
name: "Test User",
|
|
522
|
+
email: "test@example.com",
|
|
523
|
+
createdAt: new Date("2024-01-01"),
|
|
524
|
+
...overrides,
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Usage in tests
|
|
529
|
+
import { createTestUser } from "@tests/factories/user";
|
|
530
|
+
|
|
531
|
+
it("should update user email", () => {
|
|
532
|
+
const user = createTestUser({ email: "old@example.com" });
|
|
533
|
+
|
|
534
|
+
updateEmail(user, "new@example.com");
|
|
535
|
+
|
|
536
|
+
expect(user.email).toBe("new@example.com");
|
|
537
|
+
});
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### Shared Test Utilities
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
// tests/utils/assertions.ts
|
|
544
|
+
export function expectValidEmail(email: string) {
|
|
545
|
+
expect(email).toMatch(/^[^\s@]+@[^\s@]+\.[^\s@]+$/);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function expectDateInRange(date: Date, start: Date, end: Date) {
|
|
549
|
+
expect(date.getTime()).toBeGreaterThanOrEqual(start.getTime());
|
|
550
|
+
expect(date.getTime()).toBeLessThanOrEqual(end.getTime());
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Usage
|
|
554
|
+
import { expectValidEmail, expectDateInRange } from "@tests/utils/assertions";
|
|
555
|
+
|
|
556
|
+
it("should create user with valid email", () => {
|
|
557
|
+
const user = createUser("test@example.com");
|
|
558
|
+
|
|
559
|
+
expectValidEmail(user.email);
|
|
560
|
+
expectDateInRange(user.createdAt, new Date(), new Date());
|
|
561
|
+
});
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Mock Implementations
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// tests/mocks/database.ts
|
|
568
|
+
import { mock } from "bun:test";
|
|
569
|
+
|
|
570
|
+
export class MockDatabase {
|
|
571
|
+
private data: Map<string, any> = new Map();
|
|
572
|
+
|
|
573
|
+
insert = mock((key: string, value: any) => {
|
|
574
|
+
this.data.set(key, value);
|
|
575
|
+
return Promise.resolve({ id: key });
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
get = mock((key: string) => {
|
|
579
|
+
return Promise.resolve(this.data.get(key));
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
delete = mock((key: string) => {
|
|
583
|
+
this.data.delete(key);
|
|
584
|
+
return Promise.resolve();
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
clear() {
|
|
588
|
+
this.data.clear();
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Usage
|
|
593
|
+
import { MockDatabase } from "@tests/mocks/database";
|
|
594
|
+
|
|
595
|
+
it("should save user to database", async () => {
|
|
596
|
+
const db = new MockDatabase();
|
|
597
|
+
const service = new UserService(db);
|
|
598
|
+
|
|
599
|
+
await service.saveUser({ id: "1", name: "Alice" });
|
|
600
|
+
|
|
601
|
+
expect(db.insert).toHaveBeenCalledWith("1", { id: "1", name: "Alice" });
|
|
602
|
+
});
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
## Coverage Configuration
|
|
606
|
+
|
|
607
|
+
### Package.json Scripts
|
|
608
|
+
|
|
609
|
+
```json
|
|
610
|
+
{
|
|
611
|
+
"scripts": {
|
|
612
|
+
"test": "bun test",
|
|
613
|
+
"test:watch": "bun test --watch",
|
|
614
|
+
"test:coverage": "bun test --coverage",
|
|
615
|
+
"test:unit": "bun test tests/unit",
|
|
616
|
+
"test:integration": "bun test tests/integration"
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
### Coverage Thresholds
|
|
622
|
+
|
|
623
|
+
```typescript
|
|
624
|
+
// bun.test.ts or bunfig.toml
|
|
625
|
+
export default {
|
|
626
|
+
coverage: {
|
|
627
|
+
thresholds: {
|
|
628
|
+
lines: 80,
|
|
629
|
+
functions: 80,
|
|
630
|
+
branches: 80,
|
|
631
|
+
statements: 80,
|
|
632
|
+
},
|
|
633
|
+
exclude: ["tests/**", "**/*.test.ts", "**/*.spec.ts", "**/node_modules/**", "**/dist/**"],
|
|
634
|
+
},
|
|
635
|
+
};
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
## Real-World Examples
|
|
639
|
+
|
|
640
|
+
### Example 1: Testing Pure Functions
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
// src/utils/email.ts
|
|
644
|
+
export interface EmailValidationResult {
|
|
645
|
+
valid: boolean;
|
|
646
|
+
reason?: string;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function validateEmail(email: string): EmailValidationResult {
|
|
650
|
+
if (!email) {
|
|
651
|
+
return { valid: false, reason: "Email is required" };
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (!email.includes("@")) {
|
|
655
|
+
return { valid: false, reason: "Email must contain @" };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const [local, domain] = email.split("@");
|
|
659
|
+
|
|
660
|
+
if (!local || !domain) {
|
|
661
|
+
return { valid: false, reason: "Invalid email format" };
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
if (!domain.includes(".")) {
|
|
665
|
+
return { valid: false, reason: "Domain must contain ." };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return { valid: true };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// tests/unit/utils/email.test.ts
|
|
672
|
+
import { describe, it, expect } from "bun:test";
|
|
673
|
+
import { validateEmail } from "@src/utils/email";
|
|
674
|
+
|
|
675
|
+
describe("validateEmail", () => {
|
|
676
|
+
it("should reject empty email", () => {
|
|
677
|
+
const result = validateEmail("");
|
|
678
|
+
|
|
679
|
+
expect(result.valid).toBe(false);
|
|
680
|
+
expect(result.reason).toBe("Email is required");
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
it("should reject email without @", () => {
|
|
684
|
+
const result = validateEmail("invalid");
|
|
685
|
+
|
|
686
|
+
expect(result.valid).toBe(false);
|
|
687
|
+
expect(result.reason).toBe("Email must contain @");
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("should reject email without local part", () => {
|
|
691
|
+
const result = validateEmail("@example.com");
|
|
692
|
+
|
|
693
|
+
expect(result.valid).toBe(false);
|
|
694
|
+
expect(result.reason).toBe("Invalid email format");
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
it("should reject email without domain", () => {
|
|
698
|
+
const result = validateEmail("user@");
|
|
699
|
+
|
|
700
|
+
expect(result.valid).toBe(false);
|
|
701
|
+
expect(result.reason).toBe("Invalid email format");
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it("should reject domain without TLD", () => {
|
|
705
|
+
const result = validateEmail("user@example");
|
|
706
|
+
|
|
707
|
+
expect(result.valid).toBe(false);
|
|
708
|
+
expect(result.reason).toBe("Domain must contain .");
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
it("should accept valid email", () => {
|
|
712
|
+
const result = validateEmail("user@example.com");
|
|
713
|
+
|
|
714
|
+
expect(result.valid).toBe(true);
|
|
715
|
+
expect(result.reason).toBeUndefined();
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
it("should accept complex valid email", () => {
|
|
719
|
+
const result = validateEmail("user.name+tag@example.co.uk");
|
|
720
|
+
|
|
721
|
+
expect(result.valid).toBe(true);
|
|
722
|
+
});
|
|
723
|
+
});
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Example 2: Testing with Dependency Injection
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
// src/services/user-service.ts
|
|
730
|
+
export interface IUserRepository {
|
|
731
|
+
save(user: User): Promise<void>;
|
|
732
|
+
findById(id: string): Promise<User | null>;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export class UserService {
|
|
736
|
+
constructor(private repository: IUserRepository) {}
|
|
737
|
+
|
|
738
|
+
async createUser(name: string, email: string): Promise<User> {
|
|
739
|
+
const user: User = {
|
|
740
|
+
id: generateId(),
|
|
741
|
+
name,
|
|
742
|
+
email,
|
|
743
|
+
createdAt: new Date(),
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
await this.repository.save(user);
|
|
747
|
+
|
|
748
|
+
return user;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async getUser(id: string): Promise<User> {
|
|
752
|
+
const user = await this.repository.findById(id);
|
|
753
|
+
|
|
754
|
+
if (!user) {
|
|
755
|
+
throw new Error(`User ${id} not found`);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return user;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// tests/unit/services/user-service.test.ts
|
|
763
|
+
import { describe, it, expect, mock } from "bun:test";
|
|
764
|
+
import { UserService, type IUserRepository } from "@src/services/user-service";
|
|
765
|
+
import type { User } from "@src/types";
|
|
766
|
+
|
|
767
|
+
describe("UserService", () => {
|
|
768
|
+
function createMockRepository(): IUserRepository {
|
|
769
|
+
return {
|
|
770
|
+
save: mock(() => Promise.resolve()),
|
|
771
|
+
findById: mock(() => Promise.resolve(null)),
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
describe("createUser", () => {
|
|
776
|
+
it("should create and save user", async () => {
|
|
777
|
+
// Arrange
|
|
778
|
+
const mockRepo = createMockRepository();
|
|
779
|
+
const service = new UserService(mockRepo);
|
|
780
|
+
|
|
781
|
+
// Act
|
|
782
|
+
const user = await service.createUser("Alice", "alice@example.com");
|
|
783
|
+
|
|
784
|
+
// Assert
|
|
785
|
+
expect(user.name).toBe("Alice");
|
|
786
|
+
expect(user.email).toBe("alice@example.com");
|
|
787
|
+
expect(user.id).toBeDefined();
|
|
788
|
+
expect(user.createdAt).toBeInstanceOf(Date);
|
|
789
|
+
expect(mockRepo.save).toHaveBeenCalledWith(user);
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
describe("getUser", () => {
|
|
794
|
+
it("should return user when found", async () => {
|
|
795
|
+
// Arrange
|
|
796
|
+
const mockUser: User = {
|
|
797
|
+
id: "123",
|
|
798
|
+
name: "Alice",
|
|
799
|
+
email: "alice@example.com",
|
|
800
|
+
createdAt: new Date(),
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const mockRepo = createMockRepository();
|
|
804
|
+
mockRepo.findById.mockResolvedValue(mockUser);
|
|
805
|
+
|
|
806
|
+
const service = new UserService(mockRepo);
|
|
807
|
+
|
|
808
|
+
// Act
|
|
809
|
+
const user = await service.getUser("123");
|
|
810
|
+
|
|
811
|
+
// Assert
|
|
812
|
+
expect(user).toEqual(mockUser);
|
|
813
|
+
expect(mockRepo.findById).toHaveBeenCalledWith("123");
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
it("should throw error when user not found", async () => {
|
|
817
|
+
// Arrange
|
|
818
|
+
const mockRepo = createMockRepository();
|
|
819
|
+
mockRepo.findById.mockResolvedValue(null);
|
|
820
|
+
|
|
821
|
+
const service = new UserService(mockRepo);
|
|
822
|
+
|
|
823
|
+
// Act & Assert
|
|
824
|
+
await expect(service.getUser("999")).rejects.toThrow("User 999 not found");
|
|
825
|
+
expect(mockRepo.findById).toHaveBeenCalledWith("999");
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
});
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
### Example 3: Testing File System Operations
|
|
832
|
+
|
|
833
|
+
```typescript
|
|
834
|
+
// src/utils/config-loader.ts
|
|
835
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
836
|
+
import { join } from "node:path";
|
|
837
|
+
|
|
838
|
+
export interface Config {
|
|
839
|
+
apiKey: string;
|
|
840
|
+
environment: "development" | "production";
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
export function loadConfig(configPath: string): Config {
|
|
844
|
+
if (!existsSync(configPath)) {
|
|
845
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const content = readFileSync(configPath, "utf-8");
|
|
849
|
+
const config = JSON.parse(content);
|
|
850
|
+
|
|
851
|
+
if (!config.apiKey) {
|
|
852
|
+
throw new Error("Config missing apiKey");
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (!config.environment) {
|
|
856
|
+
throw new Error("Config missing environment");
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return config;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// tests/unit/utils/config-loader.test.ts
|
|
863
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
864
|
+
import { mkdtempSync, rmSync, writeFileSync, realpathSync } from "node:fs";
|
|
865
|
+
import { tmpdir } from "node:os";
|
|
866
|
+
import { join } from "node:path";
|
|
867
|
+
import { loadConfig } from "@src/utils/config-loader";
|
|
868
|
+
|
|
869
|
+
describe("loadConfig", () => {
|
|
870
|
+
let testDir: string;
|
|
871
|
+
|
|
872
|
+
beforeEach(() => {
|
|
873
|
+
testDir = realpathSync(mkdtempSync(join(tmpdir(), "config-test-")));
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
afterEach(() => {
|
|
877
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("should load valid config", () => {
|
|
881
|
+
// Arrange
|
|
882
|
+
const configPath = join(testDir, "config.json");
|
|
883
|
+
const configData = {
|
|
884
|
+
apiKey: "test-key",
|
|
885
|
+
environment: "development",
|
|
886
|
+
};
|
|
887
|
+
writeFileSync(configPath, JSON.stringify(configData), "utf-8");
|
|
888
|
+
|
|
889
|
+
// Act
|
|
890
|
+
const config = loadConfig(configPath);
|
|
891
|
+
|
|
892
|
+
// Assert
|
|
893
|
+
expect(config.apiKey).toBe("test-key");
|
|
894
|
+
expect(config.environment).toBe("development");
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
it("should throw error if file not found", () => {
|
|
898
|
+
// Arrange
|
|
899
|
+
const configPath = join(testDir, "nonexistent.json");
|
|
900
|
+
|
|
901
|
+
// Act & Assert
|
|
902
|
+
expect(() => loadConfig(configPath)).toThrow("Config file not found");
|
|
903
|
+
});
|
|
904
|
+
|
|
905
|
+
it("should throw error if apiKey missing", () => {
|
|
906
|
+
// Arrange
|
|
907
|
+
const configPath = join(testDir, "config.json");
|
|
908
|
+
writeFileSync(configPath, JSON.stringify({ environment: "development" }), "utf-8");
|
|
909
|
+
|
|
910
|
+
// Act & Assert
|
|
911
|
+
expect(() => loadConfig(configPath)).toThrow("Config missing apiKey");
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it("should throw error if environment missing", () => {
|
|
915
|
+
// Arrange
|
|
916
|
+
const configPath = join(testDir, "config.json");
|
|
917
|
+
writeFileSync(configPath, JSON.stringify({ apiKey: "test-key" }), "utf-8");
|
|
918
|
+
|
|
919
|
+
// Act & Assert
|
|
920
|
+
expect(() => loadConfig(configPath)).toThrow("Config missing environment");
|
|
921
|
+
});
|
|
922
|
+
});
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
## Common Pitfalls
|
|
926
|
+
|
|
927
|
+
### Pitfall 1: Forgetting to Restore Mocks
|
|
928
|
+
|
|
929
|
+
```typescript
|
|
930
|
+
// BAD - Mocks leak between tests
|
|
931
|
+
describe("Tests with mocks", () => {
|
|
932
|
+
it("test 1", () => {
|
|
933
|
+
const fn = mock(() => "result");
|
|
934
|
+
// Mock not restored
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
it("test 2", () => {
|
|
938
|
+
// Previous mock still active!
|
|
939
|
+
});
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
// GOOD - Restore mocks
|
|
943
|
+
describe("Tests with mocks", () => {
|
|
944
|
+
afterEach(() => {
|
|
945
|
+
mock.restore();
|
|
946
|
+
});
|
|
947
|
+
|
|
948
|
+
it("test 1", () => {
|
|
949
|
+
const fn = mock(() => "result");
|
|
950
|
+
});
|
|
951
|
+
|
|
952
|
+
it("test 2", () => {
|
|
953
|
+
// Fresh state
|
|
954
|
+
});
|
|
955
|
+
});
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
### Pitfall 2: Not Cleaning Up File System
|
|
959
|
+
|
|
960
|
+
```typescript
|
|
961
|
+
// BAD - Temporary files left behind
|
|
962
|
+
it("should write file", () => {
|
|
963
|
+
const testFile = "/tmp/test.txt";
|
|
964
|
+
writeFileSync(testFile, "test");
|
|
965
|
+
// File never deleted
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
// GOOD - Clean up after test
|
|
969
|
+
it("should write file", () => {
|
|
970
|
+
const testFile = "/tmp/test.txt";
|
|
971
|
+
|
|
972
|
+
try {
|
|
973
|
+
writeFileSync(testFile, "test");
|
|
974
|
+
// Test assertions
|
|
975
|
+
} finally {
|
|
976
|
+
if (existsSync(testFile)) {
|
|
977
|
+
rmSync(testFile);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
});
|
|
981
|
+
```
|
|
982
|
+
|
|
983
|
+
### Pitfall 3: Race Conditions with process.chdir()
|
|
984
|
+
|
|
985
|
+
```typescript
|
|
986
|
+
// BAD - Parallel tests conflict
|
|
987
|
+
describe("Directory tests", () => {
|
|
988
|
+
it("test 1", () => {
|
|
989
|
+
process.chdir("/tmp");
|
|
990
|
+
// Affects other parallel tests!
|
|
991
|
+
});
|
|
992
|
+
|
|
993
|
+
it("test 2", () => {
|
|
994
|
+
process.chdir("/var");
|
|
995
|
+
// Affects other parallel tests!
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// GOOD - Serial execution
|
|
1000
|
+
describe.serial("Directory tests", () => {
|
|
1001
|
+
let originalCwd: string;
|
|
1002
|
+
|
|
1003
|
+
beforeEach(() => {
|
|
1004
|
+
originalCwd = process.cwd();
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
afterEach(() => {
|
|
1008
|
+
process.chdir(originalCwd);
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
it("test 1", () => {
|
|
1012
|
+
process.chdir("/tmp");
|
|
1013
|
+
});
|
|
1014
|
+
|
|
1015
|
+
it("test 2", () => {
|
|
1016
|
+
process.chdir("/var");
|
|
1017
|
+
});
|
|
1018
|
+
});
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
## Best Practices Summary
|
|
1022
|
+
|
|
1023
|
+
**Test structure:**
|
|
1024
|
+
|
|
1025
|
+
- Use `describe` to group related tests
|
|
1026
|
+
- Use `beforeEach`/`afterEach` for setup/cleanup
|
|
1027
|
+
- Use path aliases (`@src/*`) for imports
|
|
1028
|
+
- One assertion per test (or related assertions)
|
|
1029
|
+
|
|
1030
|
+
**Async testing:**
|
|
1031
|
+
|
|
1032
|
+
- Always use `async/await` for promises
|
|
1033
|
+
- Use `expect().resolves` and `expect().rejects`
|
|
1034
|
+
- Set appropriate timeouts for slow operations
|
|
1035
|
+
|
|
1036
|
+
**Mocking:**
|
|
1037
|
+
|
|
1038
|
+
- Mock external dependencies (filesystem, network, database)
|
|
1039
|
+
- Use dependency injection for testability
|
|
1040
|
+
- Always restore mocks in `afterEach`
|
|
1041
|
+
- Create reusable mock factories
|
|
1042
|
+
|
|
1043
|
+
**File system:**
|
|
1044
|
+
|
|
1045
|
+
- Use temporary directories for file tests
|
|
1046
|
+
- Clean up temp files/directories in `afterEach`
|
|
1047
|
+
- Use `realpathSync` to resolve symlinks on macOS
|
|
1048
|
+
- Use `.serial` for tests using `process.chdir()`
|
|
1049
|
+
|
|
1050
|
+
**Platform testing:**
|
|
1051
|
+
|
|
1052
|
+
- Use `.skipIf()` for platform-specific tests
|
|
1053
|
+
- Test on multiple platforms in CI/CD
|
|
1054
|
+
- Mock platform-specific APIs when needed
|
|
1055
|
+
|
|
1056
|
+
**Coverage:**
|
|
1057
|
+
|
|
1058
|
+
- Aim for 80%+ on business logic
|
|
1059
|
+
- Exclude test files from coverage
|
|
1060
|
+
- Focus on meaningful tests, not coverage numbers
|
|
1061
|
+
|
|
1062
|
+
**Result:** Fast, reliable tests that enable confident refactoring and prevent regressions.
|