@yamf/test 0.1.0
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/README.md +428 -0
- package/package.json +39 -0
- package/src/assert.js +504 -0
- package/src/assertion-errors.js +114 -0
- package/src/cli.js +49 -0
- package/src/helpers.js +69 -0
- package/src/index.js +49 -0
- package/src/runner.js +329 -0
- package/src/tap-reporter.js +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
# @yamf/test
|
|
2
|
+
|
|
3
|
+
A minimal, zero-dependency testing library with polymorphic async/await and simultaneous assertion reports.
|
|
4
|
+
|
|
5
|
+
[]()
|
|
6
|
+
[]()
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
✨ **Zero Dependencies** - Pure Node.js implementation
|
|
11
|
+
🔄 **Automatic Async Detection** - Just add `await` when needed
|
|
12
|
+
📊 **Multi-Assertion Reports** - All failures reported simultaneously
|
|
13
|
+
🏃 **Sequential Test Execution** - Predictable, ordered test runs
|
|
14
|
+
🎯 **Solo/Mute Flags** - Focus on specific tests or skip others
|
|
15
|
+
📦 **Suite Support** - Organize tests into logical groups
|
|
16
|
+
🎨 **Colorized Output** - Clear pass/fail visualization
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
npm install @yamf/test
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```javascript
|
|
27
|
+
import { assert, runTests } from '@yamf/test'
|
|
28
|
+
|
|
29
|
+
function testAddition() {
|
|
30
|
+
assert(2 + 2, n => n === 4)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function testAsyncFetch() {
|
|
34
|
+
await assert(
|
|
35
|
+
async () => await fetchData(),
|
|
36
|
+
data => data.status === 'ok'
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
runTests({ testAddition, testAsyncFetch })
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Core Assertions
|
|
44
|
+
|
|
45
|
+
### `assert(value, ...assertFns)`
|
|
46
|
+
|
|
47
|
+
Assert that a value or function result passes all assertion functions.
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// Direct value
|
|
51
|
+
assert(5, n => n > 0, n => n < 10)
|
|
52
|
+
|
|
53
|
+
// Function result
|
|
54
|
+
assert(() => getValue(), n => n === 42)
|
|
55
|
+
|
|
56
|
+
// Multiple conditions
|
|
57
|
+
const user = { name: 'Alice', age: 30 }
|
|
58
|
+
assert(user,
|
|
59
|
+
u => u.name === 'Alice',
|
|
60
|
+
u => u.age > 18
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Async (auto-detected - just add await)
|
|
64
|
+
await assert(Promise.resolve(10), n => n === 10)
|
|
65
|
+
await assert(async () => await fetchData(), d => d.status === 'ok')
|
|
66
|
+
await assert(() => fetchData(), d => d.status === 'ok') // Promise-returning
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### `assertErr(error, ...assertFns)`
|
|
70
|
+
|
|
71
|
+
Assert that a function throws an error matching all assertion functions.
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
// Direct error
|
|
75
|
+
assertErr(new Error('test'), err => err.message === 'test')
|
|
76
|
+
|
|
77
|
+
// Throwing function
|
|
78
|
+
assertErr(
|
|
79
|
+
() => { throw new Error('expected') },
|
|
80
|
+
err => err.message === 'expected',
|
|
81
|
+
err => err instanceof Error
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
// Async throwing (auto-detected)
|
|
85
|
+
await assertErr(
|
|
86
|
+
async () => { throw new Error('async failure') },
|
|
87
|
+
err => err.message === 'async failure'
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Promise rejection
|
|
91
|
+
await assertErr(
|
|
92
|
+
() => Promise.reject(new Error('rejected')),
|
|
93
|
+
err => err.message === 'rejected'
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Array Assertions
|
|
98
|
+
|
|
99
|
+
### `assertEach(values, ...assertFns)`
|
|
100
|
+
|
|
101
|
+
Assert that **each value** in an array passes **all** assertion functions.
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// All values must pass all assertions
|
|
105
|
+
assertEach([1, 2, 3, 4, 5],
|
|
106
|
+
n => n > 0,
|
|
107
|
+
n => n < 10
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
// Async values
|
|
111
|
+
await assertEach([promise1, promise2, promise3],
|
|
112
|
+
r => r.status === 'ok',
|
|
113
|
+
r => r.code === 200
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### `assertSequence(values, ...assertFns)`
|
|
118
|
+
|
|
119
|
+
Assert that **each value** passes its **corresponding** assertion function.
|
|
120
|
+
|
|
121
|
+
```javascript
|
|
122
|
+
// 1:1 mapping of values to assertions
|
|
123
|
+
assertSequence(['first', 'second', 'third'],
|
|
124
|
+
v => v === 'first',
|
|
125
|
+
v => v === 'second',
|
|
126
|
+
v => v === 'third'
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
// Async sequence
|
|
130
|
+
await assertSequence([promise1, promise2, promise3],
|
|
131
|
+
n => n === 10,
|
|
132
|
+
n => n === 20,
|
|
133
|
+
n => n === 30
|
|
134
|
+
)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### `assertErrEach(errors, ...assertFns)`
|
|
138
|
+
|
|
139
|
+
Assert that **each error** in an array passes **all** assertion functions.
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
// Error objects
|
|
143
|
+
assertErrEach([
|
|
144
|
+
new Error('Connection failed'),
|
|
145
|
+
new Error('Timeout occurred')
|
|
146
|
+
],
|
|
147
|
+
err => err instanceof Error,
|
|
148
|
+
err => err.message.length > 0
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Throwing functions
|
|
152
|
+
assertErrEach([
|
|
153
|
+
() => { throw new Error('error1') },
|
|
154
|
+
() => { throw new Error('error2') }
|
|
155
|
+
],
|
|
156
|
+
err => err instanceof Error,
|
|
157
|
+
err => err.message.includes('error')
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `assertErrSequence(errors, ...assertFns)`
|
|
162
|
+
|
|
163
|
+
Assert that **each error** passes its **corresponding** assertion function.
|
|
164
|
+
|
|
165
|
+
```javascript
|
|
166
|
+
assertErrSequence([
|
|
167
|
+
() => { throw new Error('Step 1 failed') },
|
|
168
|
+
() => { throw new Error('Step 2 failed') }
|
|
169
|
+
],
|
|
170
|
+
err => err.message.includes('Step 1'),
|
|
171
|
+
err => err.message.includes('Step 2')
|
|
172
|
+
)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Test Runners
|
|
176
|
+
|
|
177
|
+
### `runTests(tests)`
|
|
178
|
+
|
|
179
|
+
Run tests directly without organizing into suites.
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
import { runTests } from '@yamf/test'
|
|
183
|
+
|
|
184
|
+
const tests = {
|
|
185
|
+
testOne() { assert(1, n => n === 1) },
|
|
186
|
+
testTwo() { assert(2, n => n === 2) }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
runTests(tests)
|
|
190
|
+
.then(() => process.exit(0))
|
|
191
|
+
.catch(err => process.exit(err.code || 1))
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### `TestRunner` Class
|
|
195
|
+
|
|
196
|
+
Organize tests into suites for better organization.
|
|
197
|
+
|
|
198
|
+
```javascript
|
|
199
|
+
import { TestRunner } from '@yamf/test'
|
|
200
|
+
|
|
201
|
+
const runner = new TestRunner()
|
|
202
|
+
|
|
203
|
+
// Add individual suite
|
|
204
|
+
runner.addSuite('math', {
|
|
205
|
+
testAddition() { assert(2 + 2, n => n === 4) },
|
|
206
|
+
testSubtraction() { assert(5 - 3, n => n === 2) }
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
// Add multiple suites
|
|
210
|
+
runner.addSuites({
|
|
211
|
+
strings: {
|
|
212
|
+
testConcat() { assert('a' + 'b', s => s === 'ab') }
|
|
213
|
+
},
|
|
214
|
+
arrays: {
|
|
215
|
+
testPush() {
|
|
216
|
+
const arr = [1, 2]
|
|
217
|
+
arr.push(3)
|
|
218
|
+
assert(arr.length, n => n === 3)
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
runner.run()
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Test Helpers
|
|
227
|
+
|
|
228
|
+
### `sleep(ms)`
|
|
229
|
+
|
|
230
|
+
Simple promise-based delay.
|
|
231
|
+
|
|
232
|
+
```javascript
|
|
233
|
+
import { sleep } from '@yamf/test'
|
|
234
|
+
|
|
235
|
+
async function testWithDelay() {
|
|
236
|
+
await sleep(100) // Wait 100ms
|
|
237
|
+
assert(true, v => v === true)
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `terminateAfter(...servers, testFn)`
|
|
242
|
+
|
|
243
|
+
Run tests with automatic server cleanup. Terminates all servers after the test completes (success or failure).
|
|
244
|
+
|
|
245
|
+
```javascript
|
|
246
|
+
import { terminateAfter } from '@yamf/test'
|
|
247
|
+
import { registryServer, createService } from '@yamf/core'
|
|
248
|
+
|
|
249
|
+
async function testServices() {
|
|
250
|
+
await terminateAfter(
|
|
251
|
+
registryServer(),
|
|
252
|
+
createService(function myService(p) { return { ok: true } }),
|
|
253
|
+
async (registry, service) => {
|
|
254
|
+
const result = await callService('myService', {})
|
|
255
|
+
assert(result.ok, v => v === true)
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### `withEnv(envVars, testFn)`
|
|
262
|
+
|
|
263
|
+
Run a test with temporary environment variables that are restored after.
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
import { withEnv } from '@yamf/test'
|
|
267
|
+
|
|
268
|
+
async function testWithEnv() {
|
|
269
|
+
await withEnv(
|
|
270
|
+
{ NODE_ENV: 'test', API_KEY: 'secret' },
|
|
271
|
+
async () => {
|
|
272
|
+
assert(process.env.NODE_ENV, v => v === 'test')
|
|
273
|
+
assert(process.env.API_KEY, v => v === 'secret')
|
|
274
|
+
}
|
|
275
|
+
)
|
|
276
|
+
// Original env vars restored here
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Solo and Mute Flags
|
|
281
|
+
|
|
282
|
+
Focus on specific tests or skip others during development.
|
|
283
|
+
|
|
284
|
+
```javascript
|
|
285
|
+
function testNormal() {
|
|
286
|
+
assert(1, n => n === 1)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Only run this test (and others marked solo)
|
|
290
|
+
function testFocused() {
|
|
291
|
+
assert(2, n => n === 2)
|
|
292
|
+
}
|
|
293
|
+
testFocused.solo = true
|
|
294
|
+
|
|
295
|
+
// Skip this test
|
|
296
|
+
function testSkipped() {
|
|
297
|
+
assert(3, n => n === 3)
|
|
298
|
+
}
|
|
299
|
+
testSkipped.mute = true
|
|
300
|
+
|
|
301
|
+
runTests({ testNormal, testFocused, testSkipped })
|
|
302
|
+
// Only testFocused will run
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Error Types
|
|
306
|
+
|
|
307
|
+
### `AssertionFailure`
|
|
308
|
+
|
|
309
|
+
Thrown when an assertion fails with details about the failure.
|
|
310
|
+
|
|
311
|
+
### `MultiAssertionFailure`
|
|
312
|
+
|
|
313
|
+
Thrown when multiple assertions fail in a single test, containing all failure details.
|
|
314
|
+
|
|
315
|
+
## Best Practices
|
|
316
|
+
|
|
317
|
+
### Name Test Functions
|
|
318
|
+
|
|
319
|
+
Named functions provide clear test output:
|
|
320
|
+
|
|
321
|
+
```javascript
|
|
322
|
+
// Good - clear output: "✔ testUserValidation"
|
|
323
|
+
function testUserValidation() {
|
|
324
|
+
assert(user.isValid, v => v === true)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Avoid - unclear output
|
|
328
|
+
const test1 = () => assert(user.isValid, v => v === true)
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Export Test Functions
|
|
332
|
+
|
|
333
|
+
Export tests for CLI usage and composability:
|
|
334
|
+
|
|
335
|
+
```javascript
|
|
336
|
+
// user-tests.js
|
|
337
|
+
export function testUserCreation() { /* ... */ }
|
|
338
|
+
export function testUserDeletion() { /* ... */ }
|
|
339
|
+
|
|
340
|
+
// run-all.js
|
|
341
|
+
import * as userTests from './user-tests.js'
|
|
342
|
+
import * as authTests from './auth-tests.js'
|
|
343
|
+
|
|
344
|
+
runTests({ ...userTests, ...authTests })
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
### Use TODO for Pending Tests
|
|
348
|
+
|
|
349
|
+
Mark incomplete tests with TODO in the name:
|
|
350
|
+
|
|
351
|
+
```javascript
|
|
352
|
+
function TODOtestNewFeature() {
|
|
353
|
+
throw new Error('TODO: implement this test')
|
|
354
|
+
}
|
|
355
|
+
// Test will be counted but not run
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
## Example Output
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
✔ testDirectValue (1ms)
|
|
362
|
+
✔ testFunctionResult (0ms)
|
|
363
|
+
✔ testAsyncFunction (15ms)
|
|
364
|
+
✘ testFailingAssertion
|
|
365
|
+
|
|
366
|
+
----- Testing Complete -----
|
|
367
|
+
✔ ✔ ✔ Success Report ✔ ✔ ✔
|
|
368
|
+
|
|
369
|
+
testDirectValue
|
|
370
|
+
testFunctionResult
|
|
371
|
+
testAsyncFunction
|
|
372
|
+
|
|
373
|
+
✘ ✘ ✘ Failure Report ✘ ✘ ✘
|
|
374
|
+
|
|
375
|
+
testFailingAssertion
|
|
376
|
+
|
|
377
|
+
testFailingAssertion failed with error: AssertionFailure: ...
|
|
378
|
+
|
|
379
|
+
----- Test Overview -----
|
|
380
|
+
ℹ tests 4
|
|
381
|
+
ℹ pass 3
|
|
382
|
+
ℹ fail 1
|
|
383
|
+
ℹ skipped 0
|
|
384
|
+
ℹ todo 0
|
|
385
|
+
ℹ duration_ms 42
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## API Reference
|
|
389
|
+
|
|
390
|
+
### Assertions
|
|
391
|
+
|
|
392
|
+
| Function | Description |
|
|
393
|
+
|----------|-------------|
|
|
394
|
+
| `assert(value, ...fns)` | Assert value passes all assertion functions |
|
|
395
|
+
| `assertErr(error, ...fns)` | Assert error matches all assertion functions |
|
|
396
|
+
| `assertEach(values, ...fns)` | Assert each value passes all assertions |
|
|
397
|
+
| `assertSequence(values, ...fns)` | Assert each value passes corresponding assertion |
|
|
398
|
+
| `assertErrEach(errors, ...fns)` | Assert each error passes all assertions |
|
|
399
|
+
| `assertErrSequence(errors, ...fns)` | Assert each error passes corresponding assertion |
|
|
400
|
+
|
|
401
|
+
### Runners
|
|
402
|
+
|
|
403
|
+
| Function | Description |
|
|
404
|
+
|----------|-------------|
|
|
405
|
+
| `runTests(tests)` | Run tests directly |
|
|
406
|
+
| `TestRunner` | Class for organizing tests into suites |
|
|
407
|
+
| `runTestFnsSequentially(fns)` | Low-level sequential test execution |
|
|
408
|
+
| `mergeAllTestsSafely(...tests)` | Merge test objects with duplicate detection |
|
|
409
|
+
|
|
410
|
+
### Helpers
|
|
411
|
+
|
|
412
|
+
| Function | Description |
|
|
413
|
+
|----------|-------------|
|
|
414
|
+
| `sleep(ms)` | Promise-based delay |
|
|
415
|
+
| `terminateAfter(...servers, fn)` | Run test with server cleanup |
|
|
416
|
+
| `withEnv(vars, fn)` | Run test with temporary env vars |
|
|
417
|
+
|
|
418
|
+
### Error Types
|
|
419
|
+
|
|
420
|
+
| Type | Description |
|
|
421
|
+
|------|-------------|
|
|
422
|
+
| `AssertionFailure` | Single assertion failure |
|
|
423
|
+
| `AssertionFailureDetail` | Detailed failure information |
|
|
424
|
+
| `MultiAssertionFailure` | Multiple assertion failures |
|
|
425
|
+
|
|
426
|
+
## License
|
|
427
|
+
|
|
428
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@yamf/test",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Functional testing module - with polymorphic async/await and simultaneous assertion reports",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"homepage": "https://github.com/mcbrumagin/yamf",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./src/index.js",
|
|
10
|
+
"./validators": "./src/validators.js",
|
|
11
|
+
"./utils": "./src/utils.js"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20.0.0"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"testing",
|
|
18
|
+
"async/await",
|
|
19
|
+
"assert",
|
|
20
|
+
"zero-dependency"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"author": "Matthew C Brumagin",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/mcbrumagin/yamf"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@yamf/core": "link:../core"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"test": "./tests/test.sh"
|
|
38
|
+
}
|
|
39
|
+
}
|