puty 0.0.3 โ 0.0.4-rc1
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 +97 -1
- package/package.json +3 -1
- package/src/mockResolver.js +152 -0
- package/src/puty.js +125 -58
- package/src/utils.js +5 -0
package/README.md
CHANGED
|
@@ -13,6 +13,7 @@ Puty is ideal for testing pure functions - functions that always return the same
|
|
|
13
13
|
- [Testing Functions](#testing-functions)
|
|
14
14
|
- [Testing Classes](#testing-classes)
|
|
15
15
|
- [Error Testing](#error-testing)
|
|
16
|
+
- [Using Mocks](#using-mocks)
|
|
16
17
|
- [Using !include Directive](#using-include-directive)
|
|
17
18
|
- [YAML Structure](#yaml-structure)
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ Puty is ideal for testing pure functions - functions that always return the same
|
|
|
21
22
|
- ๐ Write tests in simple YAML format
|
|
22
23
|
- ๐ฆ Modular test organization with `!include` directive
|
|
23
24
|
- ๐ฏ Clear separation of test data and test logic
|
|
25
|
+
- ๐งช Mock support for testing functions with dependencies
|
|
24
26
|
- โก Powered by Vitest for fast test execution
|
|
25
27
|
|
|
26
28
|
## Installation
|
|
@@ -203,6 +205,79 @@ in: [10, 0]
|
|
|
203
205
|
throws: "Division by zero"
|
|
204
206
|
```
|
|
205
207
|
|
|
208
|
+
### Using Mocks
|
|
209
|
+
|
|
210
|
+
Puty supports mocking dependencies using the `$mock:` syntax. This is useful for testing functions that have external dependencies like loggers, API clients, or callbacks.
|
|
211
|
+
|
|
212
|
+
#### Basic Mock Example
|
|
213
|
+
|
|
214
|
+
```yaml
|
|
215
|
+
file: './calculator.js'
|
|
216
|
+
group: calculator
|
|
217
|
+
---
|
|
218
|
+
suite: calculate
|
|
219
|
+
exportName: calculateWithLogger
|
|
220
|
+
---
|
|
221
|
+
case: test with mock logger
|
|
222
|
+
in:
|
|
223
|
+
- 10
|
|
224
|
+
- 5
|
|
225
|
+
- $mock:logger
|
|
226
|
+
out: 15
|
|
227
|
+
mocks:
|
|
228
|
+
logger:
|
|
229
|
+
calls:
|
|
230
|
+
- in: ['Calculating 10 + 5']
|
|
231
|
+
- in: ['Result: 15']
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### Mock Hierarchy
|
|
235
|
+
|
|
236
|
+
Mocks can be defined at three levels (case overrides suite, suite overrides global):
|
|
237
|
+
|
|
238
|
+
```yaml
|
|
239
|
+
file: './service.js'
|
|
240
|
+
group: service
|
|
241
|
+
mocks:
|
|
242
|
+
globalApi: # Global mock - available to all suites
|
|
243
|
+
calls:
|
|
244
|
+
- in: ['/default']
|
|
245
|
+
out: { status: 200 }
|
|
246
|
+
---
|
|
247
|
+
suite: userService
|
|
248
|
+
mocks:
|
|
249
|
+
api: # Suite mock - available to all cases in this suite
|
|
250
|
+
calls:
|
|
251
|
+
- in: ['/users']
|
|
252
|
+
out: { users: [] }
|
|
253
|
+
---
|
|
254
|
+
case: get user with mock
|
|
255
|
+
in: [123, $mock:api]
|
|
256
|
+
out: { id: 123, name: 'John' }
|
|
257
|
+
mocks:
|
|
258
|
+
api: # Case mock - overrides suite mock
|
|
259
|
+
calls:
|
|
260
|
+
- in: ['/users/123']
|
|
261
|
+
out: { id: 123, name: 'John' }
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
#### Testing Callbacks
|
|
265
|
+
|
|
266
|
+
Mocks are perfect for testing event-driven code:
|
|
267
|
+
|
|
268
|
+
```yaml
|
|
269
|
+
case: test event emitter
|
|
270
|
+
executions:
|
|
271
|
+
- method: on
|
|
272
|
+
in: ['data', $mock:callback]
|
|
273
|
+
- method: emit
|
|
274
|
+
in: ['data', 'hello']
|
|
275
|
+
mocks:
|
|
276
|
+
callback:
|
|
277
|
+
calls:
|
|
278
|
+
- in: ['hello']
|
|
279
|
+
```
|
|
280
|
+
|
|
206
281
|
### Using !include Directive
|
|
207
282
|
|
|
208
283
|
Puty supports the `!include` directive to modularize and reuse YAML test files. This is useful for:
|
|
@@ -271,6 +346,11 @@ Puty test files use multi-document YAML format with three types of documents:
|
|
|
271
346
|
file: './module.js' # Required: Path to JS file (relative to YAML file)
|
|
272
347
|
group: 'test-group' # Required: Test group name (or use 'name')
|
|
273
348
|
suites: ['suite1', 'suite2'] # Optional: List of suites to define
|
|
349
|
+
mocks: # Optional: Global mocks available to all suites
|
|
350
|
+
mockName:
|
|
351
|
+
calls:
|
|
352
|
+
- in: [args]
|
|
353
|
+
out: result
|
|
274
354
|
```
|
|
275
355
|
|
|
276
356
|
### 2. Suite Definition Documents
|
|
@@ -280,6 +360,11 @@ suite: 'suiteName' # Required: Suite name
|
|
|
280
360
|
exportName: 'functionName' # Optional: Export to test (defaults to suite name or 'default')
|
|
281
361
|
mode: 'class' # Optional: Set to 'class' for class testing
|
|
282
362
|
constructorArgs: [arg1] # Optional: Arguments for class constructor (class mode only)
|
|
363
|
+
mocks: # Optional: Suite-level mocks for all cases in this suite
|
|
364
|
+
mockName:
|
|
365
|
+
calls:
|
|
366
|
+
- in: [args]
|
|
367
|
+
out: result
|
|
283
368
|
```
|
|
284
369
|
|
|
285
370
|
### 3. Test Case Documents
|
|
@@ -287,9 +372,15 @@ constructorArgs: [arg1] # Optional: Arguments for class constructor (class mo
|
|
|
287
372
|
For function tests:
|
|
288
373
|
```yaml
|
|
289
374
|
case: 'test description' # Required: Test case name
|
|
290
|
-
in: [arg1, arg2] # Required: Input arguments
|
|
375
|
+
in: [arg1, arg2] # Required: Input arguments (use $mock:name for mocks)
|
|
291
376
|
out: expectedValue # Optional: Expected output (omit if testing for errors)
|
|
292
377
|
throws: 'Error message' # Optional: Expected error message
|
|
378
|
+
mocks: # Optional: Case-specific mocks
|
|
379
|
+
mockName:
|
|
380
|
+
calls: # Array of expected calls
|
|
381
|
+
- in: [args] # Expected arguments
|
|
382
|
+
out: result # Optional: Return value
|
|
383
|
+
throws: 'error' # Optional: Throw error instead
|
|
293
384
|
```
|
|
294
385
|
|
|
295
386
|
For class tests:
|
|
@@ -307,5 +398,10 @@ executions:
|
|
|
307
398
|
- method: 'getter'
|
|
308
399
|
in: []
|
|
309
400
|
out: expected
|
|
401
|
+
mocks: # Optional: Mocks for the entire test case
|
|
402
|
+
mockName:
|
|
403
|
+
calls:
|
|
404
|
+
- in: [args]
|
|
405
|
+
out: result
|
|
310
406
|
```
|
|
311
407
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "puty",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4-rc1",
|
|
4
4
|
"description": "A tooling function to test javascript functions and classes.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"js-yaml": "~4.1.0"
|
|
17
17
|
},
|
|
18
18
|
"scripts": {
|
|
19
|
+
"test": "vitest run",
|
|
20
|
+
"test:watch": "vitest run",
|
|
19
21
|
"lint": "bunx prettier src tests -c",
|
|
20
22
|
"lint:fix": "bunx prettier src tests -w",
|
|
21
23
|
"build": "bun run esbuild.js"
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Mock resolution and processing utilities
|
|
3
|
+
* This module provides functions for resolving mock references, creating mock functions,
|
|
4
|
+
* and validating mock calls in YAML-driven tests.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { vi } from "vitest";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Deep equality check for mock argument validation
|
|
11
|
+
* @param {any} a - First value to compare
|
|
12
|
+
* @param {any} b - Second value to compare
|
|
13
|
+
* @returns {boolean} True if values are deeply equal
|
|
14
|
+
*/
|
|
15
|
+
const deepEqual = (a, b) => {
|
|
16
|
+
if (a === b) return true;
|
|
17
|
+
if (a == null || b == null) return false;
|
|
18
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
19
|
+
if (a.length !== b.length) return false;
|
|
20
|
+
for (let i = 0; i < a.length; i++) {
|
|
21
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
22
|
+
}
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
26
|
+
const keysA = Object.keys(a);
|
|
27
|
+
const keysB = Object.keys(b);
|
|
28
|
+
if (keysA.length !== keysB.length) return false;
|
|
29
|
+
for (const key of keysA) {
|
|
30
|
+
if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolves mock references following hierarchy: case -> suite -> global
|
|
39
|
+
* @param {Object} caseMocks - Case-level mock definitions
|
|
40
|
+
* @param {Object} suiteMocks - Suite-level mock definitions
|
|
41
|
+
* @param {Object} globalMocks - Global-level mock definitions
|
|
42
|
+
* @returns {Object} Resolved mock definitions with hierarchy applied
|
|
43
|
+
*/
|
|
44
|
+
export const resolveMocks = (caseMocks = {}, suiteMocks = {}, globalMocks = {}) => {
|
|
45
|
+
const resolved = {};
|
|
46
|
+
|
|
47
|
+
// Apply hierarchy: global -> suite -> case (case overrides suite, suite overrides global)
|
|
48
|
+
Object.assign(resolved, globalMocks, suiteMocks, caseMocks);
|
|
49
|
+
|
|
50
|
+
return resolved;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Recursively processes values, replacing $mock: references with mock functions
|
|
55
|
+
* @param {any} value - Value to process (can be string, array, object, or primitive)
|
|
56
|
+
* @param {Object} mockFunctions - Map of mock name to mock function wrapper
|
|
57
|
+
* @returns {any} Processed value with $mock: references replaced
|
|
58
|
+
*/
|
|
59
|
+
export const processMockReferences = (value, mockFunctions) => {
|
|
60
|
+
if (typeof value === 'string' && value.startsWith('$mock:')) {
|
|
61
|
+
const mockName = value.substring(6); // Remove '$mock:' prefix
|
|
62
|
+
if (!mockFunctions[mockName]) {
|
|
63
|
+
throw new Error(`Mock '${mockName}' is referenced but not defined`);
|
|
64
|
+
}
|
|
65
|
+
return mockFunctions[mockName].mockFunction;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.map(item => processMockReferences(item, mockFunctions));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (value && typeof value === 'object') {
|
|
73
|
+
const processed = {};
|
|
74
|
+
for (const [key, val] of Object.entries(value)) {
|
|
75
|
+
processed[key] = processMockReferences(val, mockFunctions);
|
|
76
|
+
}
|
|
77
|
+
return processed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return value;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Creates a mock function with call tracking and validation
|
|
85
|
+
* @param {string} mockName - Name of the mock for error reporting
|
|
86
|
+
* @param {Object} mockDefinition - Mock definition with calls array
|
|
87
|
+
* @param {Array} mockDefinition.calls - Array of expected calls with in/out/throws
|
|
88
|
+
* @returns {Object} Mock function wrapper with validation methods
|
|
89
|
+
*/
|
|
90
|
+
export const createMockFunction = (mockName, mockDefinition) => {
|
|
91
|
+
const { calls } = mockDefinition;
|
|
92
|
+
let callIndex = 0;
|
|
93
|
+
|
|
94
|
+
const mockFn = vi.fn().mockImplementation((...args) => {
|
|
95
|
+
if (callIndex >= calls.length) {
|
|
96
|
+
throw new Error(`Mock '${mockName}' was called ${callIndex + 1} time(s) but expected exactly ${calls.length} calls`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const expectedCall = calls[callIndex];
|
|
100
|
+
|
|
101
|
+
// Validate input arguments
|
|
102
|
+
if (!deepEqual(args, expectedCall.in)) {
|
|
103
|
+
throw new Error(`Expected ${mockName}(${JSON.stringify(expectedCall.in)}) but got ${mockName}(${JSON.stringify(args)})`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
callIndex++;
|
|
107
|
+
|
|
108
|
+
if (expectedCall.throws) {
|
|
109
|
+
throw new Error(expectedCall.throws);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return expectedCall.out;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
mockFunction: mockFn,
|
|
117
|
+
expectedCalls: calls.length,
|
|
118
|
+
actualCalls: () => callIndex,
|
|
119
|
+
validate: () => {
|
|
120
|
+
if (callIndex !== calls.length) {
|
|
121
|
+
throw new Error(`Mock '${mockName}' was called ${callIndex} time(s) but expected exactly ${calls.length} calls`);
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
mockName
|
|
125
|
+
};
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Validates all mocks were called as expected
|
|
130
|
+
* @param {Object} mockFunctions - Map of mock name to mock function wrapper
|
|
131
|
+
* @throws {Error} If any mock validation fails
|
|
132
|
+
*/
|
|
133
|
+
export const validateMockCalls = (mockFunctions) => {
|
|
134
|
+
for (const mockWrapper of Object.values(mockFunctions)) {
|
|
135
|
+
mockWrapper.validate();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Creates mock functions from resolved mock definitions
|
|
141
|
+
* @param {Object} resolvedMocks - Resolved mock definitions
|
|
142
|
+
* @returns {Object} Map of mock name to mock function wrapper
|
|
143
|
+
*/
|
|
144
|
+
export const createMockFunctions = (resolvedMocks) => {
|
|
145
|
+
const mockFunctions = {};
|
|
146
|
+
|
|
147
|
+
for (const [mockName, mockDef] of Object.entries(resolvedMocks)) {
|
|
148
|
+
mockFunctions[mockName] = createMockFunction(mockName, mockDef);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return mockFunctions;
|
|
152
|
+
};
|
package/src/puty.js
CHANGED
|
@@ -9,6 +9,7 @@ import yaml from "js-yaml";
|
|
|
9
9
|
import { expect, test, describe } from "vitest";
|
|
10
10
|
|
|
11
11
|
import { traverseAllFiles, parseWithIncludes } from "./utils.js";
|
|
12
|
+
import { resolveMocks, processMockReferences, createMockFunctions, validateMockCalls } from "./mockResolver.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* File extensions that are recognized as YAML test files
|
|
@@ -43,6 +44,7 @@ export const parseYamlDocuments = (yamlContent) => {
|
|
|
43
44
|
const config = {
|
|
44
45
|
file: null,
|
|
45
46
|
group: null,
|
|
47
|
+
mocks: {},
|
|
46
48
|
suites: [],
|
|
47
49
|
};
|
|
48
50
|
|
|
@@ -52,6 +54,7 @@ export const parseYamlDocuments = (yamlContent) => {
|
|
|
52
54
|
if (doc.file) {
|
|
53
55
|
config.file = doc.file;
|
|
54
56
|
config.group = doc.group || doc.name;
|
|
57
|
+
config.mocks = doc.mocks || {};
|
|
55
58
|
if (doc.suites) {
|
|
56
59
|
config.suiteNames = doc.suites;
|
|
57
60
|
}
|
|
@@ -62,6 +65,7 @@ export const parseYamlDocuments = (yamlContent) => {
|
|
|
62
65
|
currentSuite = {
|
|
63
66
|
name: doc.suite,
|
|
64
67
|
exportName: doc.exportName || doc.suite,
|
|
68
|
+
mocks: doc.mocks || {},
|
|
65
69
|
cases: [],
|
|
66
70
|
};
|
|
67
71
|
// Only add mode and constructorArgs if mode is explicitly 'class'
|
|
@@ -72,6 +76,8 @@ export const parseYamlDocuments = (yamlContent) => {
|
|
|
72
76
|
} else if (doc.case && currentSuite) {
|
|
73
77
|
const testCase = {
|
|
74
78
|
name: doc.case,
|
|
79
|
+
mocks: doc.mocks || {},
|
|
80
|
+
resolvedMocks: null,
|
|
75
81
|
};
|
|
76
82
|
|
|
77
83
|
if (currentSuite.mode === "class") {
|
|
@@ -149,18 +155,31 @@ const setupFunctionTests = (suite) => {
|
|
|
149
155
|
out: expectedOut,
|
|
150
156
|
functionUnderTest,
|
|
151
157
|
throws,
|
|
158
|
+
mockFunctions,
|
|
152
159
|
} = testCase;
|
|
153
160
|
test(name, () => {
|
|
154
161
|
if (!functionUnderTest) {
|
|
155
162
|
throw new Error(`Function not found for test case: ${name}`);
|
|
156
163
|
}
|
|
157
164
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
165
|
+
try {
|
|
166
|
+
if (throws) {
|
|
167
|
+
// Test expects an error to be thrown
|
|
168
|
+
expect(() => functionUnderTest(...(inArg || []))).toThrow(throws);
|
|
169
|
+
} else {
|
|
170
|
+
const out = functionUnderTest(...(inArg || []));
|
|
171
|
+
expect(out).toEqual(expectedOut);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Validate mock calls after test execution
|
|
175
|
+
if (mockFunctions && Object.keys(mockFunctions).length > 0) {
|
|
176
|
+
validateMockCalls(mockFunctions);
|
|
177
|
+
}
|
|
178
|
+
} finally {
|
|
179
|
+
// Cleanup mocks after test
|
|
180
|
+
if (mockFunctions) {
|
|
181
|
+
Object.values(mockFunctions).forEach(mock => mock.mockFunction.mockClear?.());
|
|
182
|
+
}
|
|
164
183
|
}
|
|
165
184
|
});
|
|
166
185
|
}
|
|
@@ -178,7 +197,7 @@ const setupFunctionTests = (suite) => {
|
|
|
178
197
|
const setupClassTests = (suite) => {
|
|
179
198
|
const { cases, ClassUnderTest, constructorArgs } = suite;
|
|
180
199
|
for (const testCase of cases) {
|
|
181
|
-
const { name, executions } = testCase;
|
|
200
|
+
const { name, executions, mockFunctions } = testCase;
|
|
182
201
|
test(name, () => {
|
|
183
202
|
if (!ClassUnderTest) {
|
|
184
203
|
throw new Error(`Class not found for test suite: ${suite.name}`);
|
|
@@ -186,57 +205,69 @@ const setupClassTests = (suite) => {
|
|
|
186
205
|
|
|
187
206
|
const instance = new ClassUnderTest(...constructorArgs);
|
|
188
207
|
|
|
189
|
-
|
|
190
|
-
const {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
208
|
+
try {
|
|
209
|
+
for (const execution of executions) {
|
|
210
|
+
const {
|
|
211
|
+
method,
|
|
212
|
+
in: inArg,
|
|
213
|
+
out: expectedOut,
|
|
214
|
+
throws,
|
|
215
|
+
asserts,
|
|
216
|
+
} = execution;
|
|
197
217
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
218
|
+
// Validate method exists
|
|
219
|
+
if (!instance[method] || typeof instance[method] !== "function") {
|
|
220
|
+
throw new Error(`Method '${method}' not found on class instance`);
|
|
221
|
+
}
|
|
202
222
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
223
|
+
// Execute the method and check its return value
|
|
224
|
+
if (throws) {
|
|
225
|
+
expect(() => instance[method](...(inArg || []))).toThrow(throws);
|
|
226
|
+
} else {
|
|
227
|
+
const result = instance[method](...(inArg || []));
|
|
228
|
+
if (expectedOut !== undefined) {
|
|
229
|
+
expect(result).toEqual(expectedOut);
|
|
230
|
+
}
|
|
210
231
|
}
|
|
211
|
-
}
|
|
212
232
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
233
|
+
// Run assertions
|
|
234
|
+
if (asserts) {
|
|
235
|
+
for (const assertion of asserts) {
|
|
236
|
+
if (assertion.property) {
|
|
237
|
+
// Property assertion
|
|
238
|
+
const actualValue = instance[assertion.property];
|
|
239
|
+
if (assertion.op === "eq") {
|
|
240
|
+
expect(actualValue).toEqual(assertion.value);
|
|
241
|
+
}
|
|
242
|
+
// Add more operators as needed
|
|
243
|
+
} else if (assertion.method) {
|
|
244
|
+
// Method assertion
|
|
245
|
+
if (
|
|
246
|
+
!instance[assertion.method] ||
|
|
247
|
+
typeof instance[assertion.method] !== "function"
|
|
248
|
+
) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Method '${assertion.method}' not found on class instance for assertion`,
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const result = instance[assertion.method](
|
|
254
|
+
...(assertion.in || []),
|
|
231
255
|
);
|
|
256
|
+
expect(result).toEqual(assertion.out);
|
|
232
257
|
}
|
|
233
|
-
const result = instance[assertion.method](
|
|
234
|
-
...(assertion.in || []),
|
|
235
|
-
);
|
|
236
|
-
expect(result).toEqual(assertion.out);
|
|
237
258
|
}
|
|
238
259
|
}
|
|
239
260
|
}
|
|
261
|
+
|
|
262
|
+
// Validate mock calls after test execution
|
|
263
|
+
if (mockFunctions && Object.keys(mockFunctions).length > 0) {
|
|
264
|
+
validateMockCalls(mockFunctions);
|
|
265
|
+
}
|
|
266
|
+
} finally {
|
|
267
|
+
// Cleanup mocks after test
|
|
268
|
+
if (mockFunctions) {
|
|
269
|
+
Object.values(mockFunctions).forEach(mock => mock.mockFunction.mockClear?.());
|
|
270
|
+
}
|
|
240
271
|
}
|
|
241
272
|
});
|
|
242
273
|
}
|
|
@@ -262,6 +293,38 @@ export const injectFunctions = (module, originalTestConfig) => {
|
|
|
262
293
|
let functionUnderTest = module[testConfig.exportName || "default"];
|
|
263
294
|
|
|
264
295
|
for (const suite of testConfig.suites) {
|
|
296
|
+
for (const testCase of suite.cases) {
|
|
297
|
+
// Resolve mocks for this test case using hierarchy
|
|
298
|
+
testCase.resolvedMocks = resolveMocks(
|
|
299
|
+
testCase.mocks,
|
|
300
|
+
suite.mocks,
|
|
301
|
+
testConfig.mocks
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// Create mock functions from resolved mock definitions
|
|
305
|
+
testCase.mockFunctions = createMockFunctions(testCase.resolvedMocks);
|
|
306
|
+
|
|
307
|
+
// Process mock references in test inputs and outputs
|
|
308
|
+
if (testCase.in) {
|
|
309
|
+
testCase.in = processMockReferences(testCase.in, testCase.mockFunctions);
|
|
310
|
+
}
|
|
311
|
+
if (testCase.out) {
|
|
312
|
+
testCase.out = processMockReferences(testCase.out, testCase.mockFunctions);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Process mock references in class test executions
|
|
316
|
+
if (testCase.executions) {
|
|
317
|
+
for (const execution of testCase.executions) {
|
|
318
|
+
if (execution.in) {
|
|
319
|
+
execution.in = processMockReferences(execution.in, testCase.mockFunctions);
|
|
320
|
+
}
|
|
321
|
+
if (execution.out) {
|
|
322
|
+
execution.out = processMockReferences(execution.out, testCase.mockFunctions);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
265
328
|
if (suite.mode === "class") {
|
|
266
329
|
const exportName = suite.exportName || "default";
|
|
267
330
|
const exported = module[exportName];
|
|
@@ -305,15 +368,19 @@ export const injectFunctions = (module, originalTestConfig) => {
|
|
|
305
368
|
export const setupTestSuiteFromYaml = async (dirname) => {
|
|
306
369
|
const testYamlFiles = traverseAllFiles(dirname, extensions);
|
|
307
370
|
for (const file of testYamlFiles) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
path.
|
|
311
|
-
|
|
312
|
-
|
|
371
|
+
try {
|
|
372
|
+
const testConfig = parseWithIncludes(file);
|
|
373
|
+
const filepathRelativeToSpecFile = path.join(
|
|
374
|
+
path.dirname(file),
|
|
375
|
+
testConfig.file,
|
|
376
|
+
);
|
|
313
377
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
378
|
+
// testConfig.file is relative to the spec file
|
|
379
|
+
const module = await import(filepathRelativeToSpecFile);
|
|
380
|
+
const testConfigWithInjectedFunctions = injectFunctions(module, testConfig);
|
|
381
|
+
setupTestSuite(testConfigWithInjectedFunctions);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
throw error;
|
|
384
|
+
}
|
|
318
385
|
}
|
|
319
386
|
};
|
package/src/utils.js
CHANGED
|
@@ -149,6 +149,7 @@ const processDocuments = (docs) => {
|
|
|
149
149
|
const config = {
|
|
150
150
|
file: null,
|
|
151
151
|
group: null,
|
|
152
|
+
mocks: {},
|
|
152
153
|
suites: [],
|
|
153
154
|
};
|
|
154
155
|
|
|
@@ -158,6 +159,7 @@ const processDocuments = (docs) => {
|
|
|
158
159
|
if (doc.file) {
|
|
159
160
|
config.file = doc.file;
|
|
160
161
|
config.group = doc.group || doc.name;
|
|
162
|
+
config.mocks = doc.mocks || {};
|
|
161
163
|
if (doc.suites) {
|
|
162
164
|
config.suiteNames = doc.suites;
|
|
163
165
|
}
|
|
@@ -168,6 +170,7 @@ const processDocuments = (docs) => {
|
|
|
168
170
|
currentSuite = {
|
|
169
171
|
name: doc.suite,
|
|
170
172
|
exportName: doc.exportName || doc.suite,
|
|
173
|
+
mocks: doc.mocks || {},
|
|
171
174
|
cases: [],
|
|
172
175
|
};
|
|
173
176
|
// Only add mode and constructorArgs if mode is explicitly 'class'
|
|
@@ -178,6 +181,8 @@ const processDocuments = (docs) => {
|
|
|
178
181
|
} else if (doc.case && currentSuite) {
|
|
179
182
|
const testCase = {
|
|
180
183
|
name: doc.case,
|
|
184
|
+
mocks: doc.mocks || {},
|
|
185
|
+
resolvedMocks: null,
|
|
181
186
|
};
|
|
182
187
|
|
|
183
188
|
if (currentSuite.mode === "class") {
|