0
# Test Runner
1
2
Programmatic test runner for executing test files with advanced configuration options including concurrency, timeouts, and custom reporters. The runner provides fine-grained control over test execution and supports both individual file execution and batch processing.
3
4
## Capabilities
5
6
### Run Function
7
8
The main programmatic interface for running tests with configurable options.
9
10
```javascript { .api }
11
/**
12
* Programmatic test runner with configurable options
13
* @param options - Runner configuration options
14
* @returns TestsStream for monitoring test progress and results
15
*/
16
function run(options?: RunOptions): TestsStream;
17
18
interface RunOptions {
19
/** Number of concurrent test files to run. Default: 1 */
20
concurrency?: number;
21
/** Global timeout for all tests in milliseconds. Default: Infinity */
22
timeout?: number;
23
/** AbortSignal for cancelling test execution */
24
signal?: AbortSignal;
25
/** Array of test file paths to execute. Default: auto-discover */
26
files?: string[];
27
/** Inspector port for debugging test execution */
28
inspectPort?: number;
29
}
30
```
31
32
**Usage Examples:**
33
34
```javascript
35
import { run } from "test";
36
37
// Basic usage - auto-discover and run tests
38
const stream = run();
39
stream.on('test:pass', (test) => {
40
console.log(`✓ ${test.name}`);
41
});
42
stream.on('test:fail', (test) => {
43
console.log(`✗ ${test.name}: ${test.error.message}`);
44
});
45
46
// Run specific files
47
run({
48
files: ['./tests/unit/*.js', './tests/integration/*.js']
49
});
50
51
// Concurrent execution
52
run({
53
concurrency: 4,
54
timeout: 30000
55
});
56
57
// With abort signal
58
const controller = new AbortController();
59
run({
60
signal: controller.signal,
61
files: ['./long-running-test.js']
62
});
63
64
// Cancel after 10 seconds
65
setTimeout(() => controller.abort(), 10000);
66
```
67
68
### TestsStream
69
70
The runner returns a TestsStream that provides real-time test execution feedback and results.
71
72
```javascript { .api }
73
interface TestsStream extends EventEmitter {
74
/** Stream of test events and results */
75
on(event: 'test:start', listener: (test: TestInfo) => void): this;
76
on(event: 'test:pass', listener: (test: TestInfo) => void): this;
77
on(event: 'test:fail', listener: (test: TestInfo) => void): this;
78
on(event: 'test:skip', listener: (test: TestInfo) => void): this;
79
on(event: 'test:todo', listener: (test: TestInfo) => void): this;
80
on(event: 'test:diagnostic', listener: (test: TestInfo, message: string) => void): this;
81
on(event: 'end', listener: (results: TestResults) => void): this;
82
}
83
84
interface TestInfo {
85
name: string;
86
file: string;
87
line?: number;
88
column?: number;
89
duration?: number;
90
error?: Error;
91
}
92
93
interface TestResults {
94
total: number;
95
pass: number;
96
fail: number;
97
skip: number;
98
todo: number;
99
duration: number;
100
files: string[];
101
}
102
```
103
104
**Usage Examples:**
105
106
```javascript
107
import { run } from "test";
108
109
const stream = run({
110
files: ['./tests/**/*.test.js'],
111
concurrency: 2
112
});
113
114
// Handle individual test events
115
stream.on('test:start', (test) => {
116
console.log(`Starting ${test.name}...`);
117
});
118
119
stream.on('test:pass', (test) => {
120
console.log(`✓ ${test.name} (${test.duration}ms)`);
121
});
122
123
stream.on('test:fail', (test) => {
124
console.error(`✗ ${test.name}`);
125
console.error(` ${test.error.message}`);
126
if (test.error.stack) {
127
console.error(` at ${test.file}:${test.line}:${test.column}`);
128
}
129
});
130
131
stream.on('test:skip', (test) => {
132
console.log(`- ${test.name} (skipped)`);
133
});
134
135
stream.on('test:todo', (test) => {
136
console.log(`? ${test.name} (todo)`);
137
});
138
139
stream.on('test:diagnostic', (test, message) => {
140
console.log(`# ${message}`);
141
});
142
143
// Handle completion
144
stream.on('end', (results) => {
145
console.log(`\nResults:`);
146
console.log(` Total: ${results.total}`);
147
console.log(` Pass: ${results.pass}`);
148
console.log(` Fail: ${results.fail}`);
149
console.log(` Skip: ${results.skip}`);
150
console.log(` Todo: ${results.todo}`);
151
console.log(` Duration: ${results.duration}ms`);
152
153
if (results.fail > 0) {
154
process.exit(1);
155
}
156
});
157
```
158
159
## File Discovery
160
161
When no files are specified, the runner automatically discovers test files using these patterns:
162
163
1. Files in `test/` directories with `.js`, `.mjs`, or `.cjs` extensions
164
2. Files ending with `.test.js`, `.test.mjs`, or `.test.cjs`
165
3. Files ending with `.spec.js`, `.spec.mjs`, or `.spec.cjs`
166
4. Excludes `node_modules` directories
167
168
**Examples:**
169
170
```javascript
171
// These files will be auto-discovered:
172
// test/user.js
173
// test/api/auth.test.js
174
// src/utils.spec.mjs
175
// integration.test.cjs
176
177
// These will be ignored:
178
// node_modules/package/test.js
179
// src/helper.js (no test pattern)
180
```
181
182
Manual file specification overrides auto-discovery:
183
184
```javascript
185
run({
186
files: [
187
'./custom-tests/*.js',
188
'./e2e/**/*.test.js'
189
]
190
});
191
```
192
193
## Concurrency Control
194
195
The runner supports concurrent execution of test files to improve performance:
196
197
```javascript
198
// Sequential execution (default)
199
run({ concurrency: 1 });
200
201
// Parallel execution - 4 files at once
202
run({ concurrency: 4 });
203
204
// Unlimited concurrency
205
run({ concurrency: Infinity });
206
207
// Boolean concurrency (uses CPU count)
208
run({ concurrency: true });
209
```
210
211
**Important Notes:**
212
- Concurrency applies to test files, not individual tests within files
213
- Individual test concurrency is controlled by test options
214
- Higher concurrency may reveal race conditions in tests
215
216
## Timeout Configuration
217
218
Set global timeouts for test execution:
219
220
```javascript
221
// 30-second timeout for all tests
222
run({ timeout: 30000 });
223
224
// No timeout (default)
225
run({ timeout: Infinity });
226
```
227
228
Timeout behavior:
229
- Applies to individual test files, not the entire run
230
- Files that timeout are marked as failed
231
- Other files continue to execute
232
233
## Debugging Support
234
235
Enable debugging for test execution:
236
237
```javascript
238
// Run with Node.js inspector
239
run({
240
inspectPort: 9229,
241
files: ['./debug-this-test.js']
242
});
243
244
// Then connect with Chrome DevTools or VS Code
245
```
246
247
## Error Handling and Cancellation
248
249
```javascript
250
import { run } from "test";
251
252
const controller = new AbortController();
253
const stream = run({
254
files: ['./tests/**/*.js'],
255
signal: controller.signal
256
});
257
258
// Handle stream errors
259
stream.on('error', (error) => {
260
console.error('Runner error:', error);
261
});
262
263
// Cancel execution
264
setTimeout(() => {
265
console.log('Cancelling tests...');
266
controller.abort();
267
}, 5000);
268
269
// Handle cancellation
270
controller.signal.addEventListener('abort', () => {
271
console.log('Test execution cancelled');
272
});
273
```
274
275
## Integration Examples
276
277
### Custom Test Reporter
278
279
```javascript
280
import { run } from "test";
281
import fs from "fs";
282
283
const results = [];
284
const stream = run({ files: ['./tests/**/*.js'] });
285
286
stream.on('test:pass', (test) => {
287
results.push({ status: 'pass', name: test.name, duration: test.duration });
288
});
289
290
stream.on('test:fail', (test) => {
291
results.push({
292
status: 'fail',
293
name: test.name,
294
error: test.error.message,
295
duration: test.duration
296
});
297
});
298
299
stream.on('end', () => {
300
// Write custom report
301
fs.writeFileSync('test-results.json', JSON.stringify(results, null, 2));
302
});
303
```
304
305
### CI/CD Integration
306
307
```javascript
308
import { run } from "test";
309
310
async function runTests() {
311
return new Promise((resolve, reject) => {
312
const stream = run({
313
files: process.env.TEST_FILES?.split(',') || undefined,
314
concurrency: parseInt(process.env.TEST_CONCURRENCY) || 1,
315
timeout: parseInt(process.env.TEST_TIMEOUT) || 30000
316
});
317
318
const results = { pass: 0, fail: 0, skip: 0, todo: 0 };
319
320
stream.on('test:pass', () => results.pass++);
321
stream.on('test:fail', () => results.fail++);
322
stream.on('test:skip', () => results.skip++);
323
stream.on('test:todo', () => results.todo++);
324
325
stream.on('end', (finalResults) => {
326
console.log(`Tests completed: ${finalResults.pass}/${finalResults.total} passed`);
327
328
if (finalResults.fail > 0) {
329
reject(new Error(`${finalResults.fail} tests failed`));
330
} else {
331
resolve(finalResults);
332
}
333
});
334
335
stream.on('error', reject);
336
});
337
}
338
339
// Usage in CI
340
runTests()
341
.then(() => process.exit(0))
342
.catch(() => process.exit(1));
343
```
344
345
### Watch Mode Implementation
346
347
```javascript
348
import { run } from "test";
349
import { watch } from "fs";
350
351
let currentRun = null;
352
353
function runTests() {
354
if (currentRun) {
355
currentRun.abort();
356
}
357
358
const controller = new AbortController();
359
currentRun = controller;
360
361
const stream = run({
362
signal: controller.signal,
363
files: ['./src/**/*.test.js']
364
});
365
366
stream.on('end', (results) => {
367
console.log(`\nWatching for changes... (${results.pass}/${results.total} passed)`);
368
currentRun = null;
369
});
370
371
stream.on('error', (error) => {
372
if (error.name !== 'AbortError') {
373
console.error('Run error:', error);
374
}
375
currentRun = null;
376
});
377
}
378
379
// Initial run
380
runTests();
381
382
// Watch for file changes
383
watch('./src', { recursive: true }, (eventType, filename) => {
384
if (filename.endsWith('.js')) {
385
console.log(`\nFile changed: ${filename}`);
386
runTests();
387
}
388
});
389
```