lil-mocky 1.4.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/.claude/settings.local.json +7 -0
- package/CLAUDE.md +191 -0
- package/Dockerfile +2 -0
- package/README.md +705 -0
- package/TODO +57 -0
- package/init-docker.sh +4 -0
- package/lil-mocky.sublime-project +8 -0
- package/lil-mocky.sublime-workspace +2405 -0
- package/package.json +14 -0
- package/src/lil-mocky.js +329 -0
- package/test/lilMockyTest.js +867 -0
- package/test-docker.sh +3 -0
package/README.md
ADDED
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
# lil-mocky
|
|
2
|
+
|
|
3
|
+
A lightweight JavaScript mocking library for testing. Create mock functions, objects, classes, and properties with call tracking and return value control. Includes spy functionality for tracking calls to existing methods.
|
|
4
|
+
|
|
5
|
+
## ðŊ Features
|
|
6
|
+
|
|
7
|
+
- ðŠķ **Lightweight**: Minimal dependencies, focused on core mocking functionality
|
|
8
|
+
- ð§ **Flexible**: Builder pattern for composable mock configuration
|
|
9
|
+
- ð **Call Tracking**: Automatic tracking of all function calls with deep-cloned arguments
|
|
10
|
+
- ð **Spy Support**: Track calls to existing methods while preserving or replacing behavior
|
|
11
|
+
- ðïļ **Class Mocks**: Per-instance mock configuration for complex class testing
|
|
12
|
+
- â
**Simple API**: Clean, intuitive API that's easy to learn and use
|
|
13
|
+
|
|
14
|
+
## ðĨ Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install lil-mocky
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## ð Quick Start
|
|
21
|
+
|
|
22
|
+
```javascript
|
|
23
|
+
const { expect } = require('chai');
|
|
24
|
+
const mocky = require('lil-mocky');
|
|
25
|
+
|
|
26
|
+
describe('User Service', () => {
|
|
27
|
+
it('calls the callback with user data', () => {
|
|
28
|
+
// Create a mock function
|
|
29
|
+
const callback = mocky.function().args('user').build();
|
|
30
|
+
|
|
31
|
+
// Call it in your code
|
|
32
|
+
callback({ name: 'Alice', age: 30 });
|
|
33
|
+
|
|
34
|
+
// Verify it was called correctly
|
|
35
|
+
expect(callback.calls(0)).to.deep.equal({
|
|
36
|
+
user: { name: 'Alice', age: 30 }
|
|
37
|
+
});
|
|
38
|
+
expect(callback.calls().length).to.equal(1);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
### ðĻ Function Mocks
|
|
46
|
+
|
|
47
|
+
Mock functions for testing callbacks and handlers:
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
// Testing code that accepts a callback
|
|
51
|
+
function processUsers(users, onComplete) {
|
|
52
|
+
const processed = users.map(u => ({ ...u, processed: true }));
|
|
53
|
+
onComplete(processed);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create mock callback with named arguments
|
|
57
|
+
const onComplete = mocky.function().args('result').build();
|
|
58
|
+
|
|
59
|
+
// Run the code under test
|
|
60
|
+
processUsers([{ name: 'Alice' }], onComplete);
|
|
61
|
+
|
|
62
|
+
// Verify the callback was called correctly
|
|
63
|
+
expect(onComplete.calls(0)).to.deep.equal({
|
|
64
|
+
result: [{ name: 'Alice', processed: true }]
|
|
65
|
+
});
|
|
66
|
+
expect(onComplete.calls().length).to.equal(1);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Common patterns:**
|
|
70
|
+
|
|
71
|
+
```javascript
|
|
72
|
+
// Mock with return value
|
|
73
|
+
const mock = mocky.function().build();
|
|
74
|
+
mock.ret('success');
|
|
75
|
+
const result = mock();
|
|
76
|
+
expect(result).to.equal('success');
|
|
77
|
+
|
|
78
|
+
// Mock async functions
|
|
79
|
+
const fetchData = mocky.function().args('url').async().build();
|
|
80
|
+
fetchData.ret({ data: [1, 2, 3] });
|
|
81
|
+
const result = await fetchData('/api/data');
|
|
82
|
+
expect(result.data).to.deep.equal([1, 2, 3]);
|
|
83
|
+
|
|
84
|
+
// Arguments with defaults
|
|
85
|
+
const logger = mocky.function().args('message', { level: 'info' }).build();
|
|
86
|
+
logger('Test message');
|
|
87
|
+
expect(logger.calls(0)).to.deep.equal({
|
|
88
|
+
message: 'Test message',
|
|
89
|
+
level: 'info'
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
#### .ret() - Set Return Values
|
|
94
|
+
|
|
95
|
+
Configure what the mock returns when called:
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
const mock = mocky.function().build();
|
|
99
|
+
|
|
100
|
+
// Simple return value
|
|
101
|
+
mock.ret('hello');
|
|
102
|
+
mock(); // Returns 'hello'
|
|
103
|
+
|
|
104
|
+
// Any value type
|
|
105
|
+
mock.ret(null);
|
|
106
|
+
mock.ret(42);
|
|
107
|
+
mock.ret([1, 2, 3]);
|
|
108
|
+
mock.ret({ data: 'value' });
|
|
109
|
+
|
|
110
|
+
// Different return per call (call numbers are 1-indexed)
|
|
111
|
+
mock.ret('first', 1); // First call
|
|
112
|
+
mock.ret('second', 2); // Second call
|
|
113
|
+
mock.ret('default'); // All other calls (call 0 is the default)
|
|
114
|
+
|
|
115
|
+
mock(); // 'first'
|
|
116
|
+
mock(); // 'second'
|
|
117
|
+
mock(); // 'default'
|
|
118
|
+
mock(); // 'default'
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Important:** `.ret()` stores the value - it doesn't call functions:
|
|
122
|
+
|
|
123
|
+
```javascript
|
|
124
|
+
// â WRONG - returns the function itself
|
|
125
|
+
mock.ret(() => compute());
|
|
126
|
+
|
|
127
|
+
// â
RIGHT - use custom implementation for dynamic behavior
|
|
128
|
+
const mock = mocky.function((context) => {
|
|
129
|
+
return compute();
|
|
130
|
+
}).build();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Throwing errors:**
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
mock.ret(new Error('Something went wrong'));
|
|
137
|
+
mock(); // Throws 'Something went wrong'
|
|
138
|
+
|
|
139
|
+
// Different errors per call
|
|
140
|
+
mock.ret(new Error('First error'), 1);
|
|
141
|
+
mock.ret(new Error('Second error'), 2);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### .calls() - Verify Arguments
|
|
145
|
+
|
|
146
|
+
Check what arguments were passed to the mock:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
const mock = mocky.function().args('name', 'age').build();
|
|
150
|
+
|
|
151
|
+
mock('Alice', 30);
|
|
152
|
+
mock('Bob', 25);
|
|
153
|
+
|
|
154
|
+
// Get specific call
|
|
155
|
+
mock.calls(0); // { name: 'Alice', age: 30 }
|
|
156
|
+
mock.calls(1); // { name: 'Bob', age: 25 }
|
|
157
|
+
|
|
158
|
+
// Get all calls
|
|
159
|
+
mock.calls(); // [{ name: 'Alice', age: 30 }, { name: 'Bob', age: 25 }]
|
|
160
|
+
mock.calls().length; // 2
|
|
161
|
+
|
|
162
|
+
// Without .args() config, returns raw arguments array
|
|
163
|
+
const rawMock = mocky.function().build();
|
|
164
|
+
rawMock('a', 'b', 'c');
|
|
165
|
+
rawMock.calls(0); // ['a', 'b', 'c']
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### .reset() - Clear State
|
|
169
|
+
|
|
170
|
+
Reset the mock back to its initial state:
|
|
171
|
+
|
|
172
|
+
```javascript
|
|
173
|
+
const mock = mocky.function().build();
|
|
174
|
+
mock.ret('value');
|
|
175
|
+
mock('test');
|
|
176
|
+
|
|
177
|
+
mock.calls().length; // 1
|
|
178
|
+
|
|
179
|
+
mock.reset();
|
|
180
|
+
|
|
181
|
+
// Everything cleared
|
|
182
|
+
mock.calls().length; // 0
|
|
183
|
+
mock(); // Returns undefined (ret cleared)
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### Using context for Custom Implementations
|
|
187
|
+
|
|
188
|
+
For dynamic behavior, pass a function that receives a `context` object:
|
|
189
|
+
|
|
190
|
+
```javascript
|
|
191
|
+
const mock = mocky.function((context) => {
|
|
192
|
+
// Your custom logic here
|
|
193
|
+
return context.args.x * 2;
|
|
194
|
+
}).args('x').build();
|
|
195
|
+
|
|
196
|
+
mock(5); // Returns 10
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Context properties:**
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
const mock = mocky.function((context) => {
|
|
203
|
+
// context.args - Named arguments (from .args() config)
|
|
204
|
+
// context.ret - Value set via .ret()
|
|
205
|
+
// context.call - Call number (1-indexed)
|
|
206
|
+
// context.self - The 'this' context
|
|
207
|
+
// context.state - Internal state object (for storing custom data)
|
|
208
|
+
|
|
209
|
+
return someValue;
|
|
210
|
+
}).args('param1', 'param2').build();
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
**Override pattern** - Allow `.ret()` to override default behavior:
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
const compute = mocky.function((context) => {
|
|
217
|
+
// Check if .ret() was called
|
|
218
|
+
if (context.ret !== undefined)
|
|
219
|
+
return context.ret;
|
|
220
|
+
|
|
221
|
+
// Default logic
|
|
222
|
+
return context.args.x * context.args.y;
|
|
223
|
+
}).args('x', 'y').build();
|
|
224
|
+
|
|
225
|
+
compute(5, 3); // Returns 15 (default logic)
|
|
226
|
+
|
|
227
|
+
compute.ret(999); // Override
|
|
228
|
+
compute(5, 3); // Returns 999
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Conditional behavior** - Different logic based on arguments:
|
|
232
|
+
|
|
233
|
+
```javascript
|
|
234
|
+
const validator = mocky.function((context) => {
|
|
235
|
+
if (context.args.value === null)
|
|
236
|
+
throw new Error('Value cannot be null');
|
|
237
|
+
|
|
238
|
+
if (context.args.value.length < 3)
|
|
239
|
+
return { valid: false, error: 'Too short' };
|
|
240
|
+
|
|
241
|
+
return { valid: true };
|
|
242
|
+
}).args('value').build();
|
|
243
|
+
|
|
244
|
+
validator('ab'); // { valid: false, error: 'Too short' }
|
|
245
|
+
validator('abc'); // { valid: true }
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Stateful mocks** - Track state across calls:
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
const counter = mocky.function((context) => {
|
|
252
|
+
// Use context.state to store data between calls
|
|
253
|
+
if (!context.state.count)
|
|
254
|
+
context.state.count = 0;
|
|
255
|
+
|
|
256
|
+
context.state.count++;
|
|
257
|
+
return context.state.count;
|
|
258
|
+
}).build();
|
|
259
|
+
|
|
260
|
+
counter(); // 1
|
|
261
|
+
counter(); // 2
|
|
262
|
+
counter(); // 3
|
|
263
|
+
|
|
264
|
+
counter.reset(); // Clears context.state
|
|
265
|
+
counter(); // 1
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**Call-specific behavior** - Different logic per call:
|
|
269
|
+
|
|
270
|
+
```javascript
|
|
271
|
+
const fetcher = mocky.function((context) => {
|
|
272
|
+
if (context.call === 1)
|
|
273
|
+
return { status: 'loading' };
|
|
274
|
+
|
|
275
|
+
if (context.call === 2)
|
|
276
|
+
return { status: 'success', data: [1, 2, 3] };
|
|
277
|
+
|
|
278
|
+
return { status: 'cached' };
|
|
279
|
+
}).build();
|
|
280
|
+
|
|
281
|
+
fetcher(); // { status: 'loading' }
|
|
282
|
+
fetcher(); // { status: 'success', ... }
|
|
283
|
+
fetcher(); // { status: 'cached' }
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
### ðĶ Object Mocks
|
|
289
|
+
|
|
290
|
+
Mock objects for testing APIs, databases, and services:
|
|
291
|
+
|
|
292
|
+
```javascript
|
|
293
|
+
// Mock an API client
|
|
294
|
+
const api = mocky.object({
|
|
295
|
+
get: mocky.function().args('url'),
|
|
296
|
+
post: mocky.function().args('url', 'data'),
|
|
297
|
+
baseURL: 'https://api.example.com',
|
|
298
|
+
timeout: 5000
|
|
299
|
+
}).build();
|
|
300
|
+
|
|
301
|
+
// Configure responses
|
|
302
|
+
api.get.ret({ status: 200, data: { users: [] } });
|
|
303
|
+
api.post.ret({ status: 201, data: { id: 123 } });
|
|
304
|
+
|
|
305
|
+
// Test your code that uses the API
|
|
306
|
+
async function createUser(apiClient, userData) {
|
|
307
|
+
const response = await apiClient.post('/users', userData);
|
|
308
|
+
return response.data;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const result = await createUser(api, { name: 'Alice' });
|
|
312
|
+
|
|
313
|
+
// Verify the API was called correctly
|
|
314
|
+
expect(api.post.calls(0)).to.deep.equal({
|
|
315
|
+
url: '/users',
|
|
316
|
+
data: { name: 'Alice' }
|
|
317
|
+
});
|
|
318
|
+
expect(result).to.deep.equal({ id: 123 });
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Immutability after .build():**
|
|
322
|
+
|
|
323
|
+
Once you call `.build()`, the mock object's structure is immutable. You cannot add or reassign properties:
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
// â WRONG - can't modify after .build()
|
|
327
|
+
const mock = mocky.object({
|
|
328
|
+
method: mocky.function()
|
|
329
|
+
}).build();
|
|
330
|
+
|
|
331
|
+
mock.method = newImplementation; // TypeError: Cannot assign to read only property
|
|
332
|
+
mock.newMethod = mocky.function().build(); // TypeError: Cannot add property
|
|
333
|
+
|
|
334
|
+
// â
RIGHT - define everything when building
|
|
335
|
+
const mock = mocky.object({
|
|
336
|
+
method: mocky.function((context) => {
|
|
337
|
+
// Your custom implementation here
|
|
338
|
+
return 'result';
|
|
339
|
+
})
|
|
340
|
+
}).build();
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
This immutability prevents bugs and ensures `.reset()` works correctly. To change behavior during tests, use `.ret()` or `.reset()` instead of reassigning properties.
|
|
344
|
+
|
|
345
|
+
**Nested mocks for complex structures:**
|
|
346
|
+
|
|
347
|
+
```javascript
|
|
348
|
+
const db = mocky.object({
|
|
349
|
+
users: mocky.object({
|
|
350
|
+
findById: mocky.function().args('id'),
|
|
351
|
+
create: mocky.function().args('userData')
|
|
352
|
+
}),
|
|
353
|
+
connected: true
|
|
354
|
+
}).build();
|
|
355
|
+
|
|
356
|
+
db.users.findById.ret({ id: 1, name: 'Alice' });
|
|
357
|
+
const user = await db.users.findById(1);
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Symbol properties (advanced):**
|
|
361
|
+
|
|
362
|
+
```javascript
|
|
363
|
+
const iterable = mocky.object({
|
|
364
|
+
[Symbol.iterator]: mocky.function()
|
|
365
|
+
}).build();
|
|
366
|
+
|
|
367
|
+
iterable[Symbol.iterator].ret('iterator');
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
#### Object .reset() Behavior
|
|
371
|
+
|
|
372
|
+
Calling `.reset()` on an object mock:
|
|
373
|
+
- Calls `.reset()` on all nested mocks (clears calls and return values)
|
|
374
|
+
- Restores plain properties to their initial values
|
|
375
|
+
- Deletes any properties added after creation
|
|
376
|
+
|
|
377
|
+
```javascript
|
|
378
|
+
const api = mocky.object({
|
|
379
|
+
get: mocky.function(),
|
|
380
|
+
baseURL: 'https://api.example.com',
|
|
381
|
+
timeout: 5000
|
|
382
|
+
}).build();
|
|
383
|
+
|
|
384
|
+
api.get.ret('response');
|
|
385
|
+
api.get('test');
|
|
386
|
+
api.baseURL = 'https://other.com';
|
|
387
|
+
api.timeout = 10000;
|
|
388
|
+
api.newProp = 'added';
|
|
389
|
+
|
|
390
|
+
api.reset();
|
|
391
|
+
|
|
392
|
+
// After reset:
|
|
393
|
+
// - api.get.calls() is []
|
|
394
|
+
// - api.get return values cleared
|
|
395
|
+
// - api.baseURL is 'https://api.example.com' (restored)
|
|
396
|
+
// - api.timeout is 5000 (restored)
|
|
397
|
+
// - api.newProp is deleted
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
### ðïļ Class Mocks
|
|
403
|
+
|
|
404
|
+
Mock classes with per-instance behavior - perfect for services that get instantiated:
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
// Mock a Logger class that gets instantiated per module
|
|
408
|
+
const Logger = mocky.class({
|
|
409
|
+
constructor: mocky.function().args('moduleName'),
|
|
410
|
+
info: mocky.function().args('message'),
|
|
411
|
+
error: mocky.function().args('message'),
|
|
412
|
+
level: 'info'
|
|
413
|
+
}).build();
|
|
414
|
+
|
|
415
|
+
// Test code that creates multiple logger instances
|
|
416
|
+
class UserService {
|
|
417
|
+
constructor() {
|
|
418
|
+
this.logger = new Logger('UserService');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async createUser(data) {
|
|
422
|
+
this.logger.info('Creating user');
|
|
423
|
+
// ... create user logic
|
|
424
|
+
return { id: 1, ...data };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
class AuthService {
|
|
429
|
+
constructor() {
|
|
430
|
+
this.logger = new Logger('AuthService');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
login(username) {
|
|
434
|
+
this.logger.info('User logging in');
|
|
435
|
+
// ... auth logic
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Create the services (each creates its own Logger instance)
|
|
440
|
+
const userService = new UserService();
|
|
441
|
+
const authService = new AuthService();
|
|
442
|
+
|
|
443
|
+
await userService.createUser({ name: 'Alice' });
|
|
444
|
+
authService.login('alice');
|
|
445
|
+
|
|
446
|
+
// Verify each instance was used correctly
|
|
447
|
+
expect(Logger.inst(0).constructor.calls(0)).to.deep.equal({
|
|
448
|
+
moduleName: 'UserService'
|
|
449
|
+
});
|
|
450
|
+
expect(Logger.inst(0).info.calls(0)).to.deep.equal({
|
|
451
|
+
message: 'Creating user'
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
expect(Logger.inst(1).constructor.calls(0)).to.deep.equal({
|
|
455
|
+
moduleName: 'AuthService'
|
|
456
|
+
});
|
|
457
|
+
expect(Logger.inst(1).info.calls(0)).to.deep.equal({
|
|
458
|
+
message: 'User logging in'
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
expect(Logger.numInsts()).to.equal(2);
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Accessing mock helpers on instances:**
|
|
465
|
+
|
|
466
|
+
You can access `.calls()`, `.ret()`, and `.reset()` directly on instance methods:
|
|
467
|
+
|
|
468
|
+
```javascript
|
|
469
|
+
const Logger = mocky.class({
|
|
470
|
+
info: mocky.function().args('message')
|
|
471
|
+
}).build();
|
|
472
|
+
|
|
473
|
+
const logger = new Logger();
|
|
474
|
+
logger.info('test message');
|
|
475
|
+
|
|
476
|
+
// Access calls directly on the instance
|
|
477
|
+
expect(logger.info.calls(0)).to.deep.equal({ message: 'test message' });
|
|
478
|
+
|
|
479
|
+
// Configure returns on the instance
|
|
480
|
+
logger.info.ret('logged');
|
|
481
|
+
expect(logger.info('another')).to.equal('logged');
|
|
482
|
+
|
|
483
|
+
// Reset via instance
|
|
484
|
+
logger.info.reset();
|
|
485
|
+
expect(logger.info.calls().length).to.equal(0);
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
**Pre-configuring instances:**
|
|
489
|
+
|
|
490
|
+
```javascript
|
|
491
|
+
// Configure behavior BEFORE instances are created
|
|
492
|
+
const Database = mocky.class({
|
|
493
|
+
constructor: mocky.function().args('connectionString'),
|
|
494
|
+
query: mocky.function().args('sql'),
|
|
495
|
+
pool: mocky.object({
|
|
496
|
+
acquire: mocky.function(),
|
|
497
|
+
release: mocky.function()
|
|
498
|
+
})
|
|
499
|
+
}).build();
|
|
500
|
+
|
|
501
|
+
// First instance returns user data
|
|
502
|
+
Database.inst(0).query.ret([{ id: 1, name: 'Alice' }]);
|
|
503
|
+
// Second instance returns empty results
|
|
504
|
+
Database.inst(1).query.ret([]);
|
|
505
|
+
|
|
506
|
+
const db1 = new Database('postgres://primary');
|
|
507
|
+
const db2 = new Database('postgres://replica');
|
|
508
|
+
|
|
509
|
+
const users = await db1.query('SELECT * FROM users'); // [{ id: 1, ... }]
|
|
510
|
+
const empty = await db2.query('SELECT * FROM users'); // []
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
#### Using context.self for Instance State
|
|
514
|
+
|
|
515
|
+
Use `context.self` to access and modify instance properties:
|
|
516
|
+
|
|
517
|
+
```javascript
|
|
518
|
+
// Mock a class where methods need to access instance properties
|
|
519
|
+
const Counter = mocky.class({
|
|
520
|
+
constructor: mocky.function((context) => {
|
|
521
|
+
// Store initial value on the instance
|
|
522
|
+
context.self._count = context.args.initial || 0;
|
|
523
|
+
}).args('initial'),
|
|
524
|
+
|
|
525
|
+
increment: mocky.function((context) => {
|
|
526
|
+
// Access instance state via context.self
|
|
527
|
+
context.self._count++;
|
|
528
|
+
return context.self._count;
|
|
529
|
+
}),
|
|
530
|
+
|
|
531
|
+
getCount: mocky.function((context) => {
|
|
532
|
+
return context.self._count;
|
|
533
|
+
})
|
|
534
|
+
}).build();
|
|
535
|
+
|
|
536
|
+
const counter = new Counter(10);
|
|
537
|
+
counter.increment(); // 11
|
|
538
|
+
counter.increment(); // 12
|
|
539
|
+
counter.getCount(); // 12
|
|
540
|
+
|
|
541
|
+
// Each instance has its own state
|
|
542
|
+
const counter2 = new Counter(100);
|
|
543
|
+
counter2.increment(); // 101
|
|
544
|
+
counter.getCount(); // Still 12
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
#### Class .reset() Behavior
|
|
548
|
+
|
|
549
|
+
Calling `.reset()` on a class mock:
|
|
550
|
+
- Clears all instance configurations
|
|
551
|
+
- Resets instance counter to 0
|
|
552
|
+
- Next instantiation starts fresh at instance 0
|
|
553
|
+
|
|
554
|
+
```javascript
|
|
555
|
+
const Mock = mocky.class({
|
|
556
|
+
method: mocky.function()
|
|
557
|
+
}).build();
|
|
558
|
+
|
|
559
|
+
Mock.inst(0).method.ret('value');
|
|
560
|
+
const instance1 = new Mock();
|
|
561
|
+
const instance2 = new Mock();
|
|
562
|
+
|
|
563
|
+
Mock.numInsts(); // 2
|
|
564
|
+
|
|
565
|
+
Mock.reset();
|
|
566
|
+
|
|
567
|
+
// After reset:
|
|
568
|
+
// - All instance configurations cleared
|
|
569
|
+
// - Mock.numInsts() is 0
|
|
570
|
+
// - Next instantiation starts fresh at instance 0
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
### ð Spy Function
|
|
576
|
+
|
|
577
|
+
Track calls to existing methods without breaking their behavior:
|
|
578
|
+
|
|
579
|
+
```javascript
|
|
580
|
+
// Spy on an existing object's method
|
|
581
|
+
const emailService = {
|
|
582
|
+
send: (to, subject, body) => {
|
|
583
|
+
// Real implementation that sends email
|
|
584
|
+
console.log(`Sending email to ${to}`);
|
|
585
|
+
return { sent: true, id: '12345' };
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
// Create a spy - calls through to original by default
|
|
590
|
+
const spy = mocky.spy(emailService, 'send');
|
|
591
|
+
|
|
592
|
+
// Test code that uses the email service
|
|
593
|
+
function notifyUser(user, message) {
|
|
594
|
+
return emailService.send(user.email, 'Notification', message);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const result = notifyUser({ email: 'alice@example.com' }, 'Hello!');
|
|
598
|
+
|
|
599
|
+
// Verify the method was called correctly
|
|
600
|
+
expect(spy.calls(0)).to.deep.equal([
|
|
601
|
+
'alice@example.com',
|
|
602
|
+
'Notification',
|
|
603
|
+
'Hello!'
|
|
604
|
+
]);
|
|
605
|
+
expect(result).to.deep.equal({ sent: true, id: '12345' });
|
|
606
|
+
|
|
607
|
+
// Clean up
|
|
608
|
+
spy.restore();
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
**Override return value:**
|
|
612
|
+
|
|
613
|
+
```javascript
|
|
614
|
+
const cache = {
|
|
615
|
+
get: (key) => localStorage.getItem(key)
|
|
616
|
+
};
|
|
617
|
+
|
|
618
|
+
const spy = mocky.spy(cache, 'get');
|
|
619
|
+
spy.ret('mocked-value'); // Override return value
|
|
620
|
+
|
|
621
|
+
const value = cache.get('user-data'); // Returns 'mocked-value'
|
|
622
|
+
spy.restore();
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
**Spy with custom replacement:**
|
|
626
|
+
|
|
627
|
+
When using a custom replacement, context includes additional properties for working with the original function:
|
|
628
|
+
|
|
629
|
+
```javascript
|
|
630
|
+
const analytics = {
|
|
631
|
+
track: (event, data) => {
|
|
632
|
+
// Real implementation posts to analytics server
|
|
633
|
+
fetch('/analytics', { method: 'POST', body: JSON.stringify({ event, data }) });
|
|
634
|
+
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
const spy = mocky.spy(analytics, 'track', mocky.function((context) => {
|
|
638
|
+
// context.original - The original function
|
|
639
|
+
// context.rawArgs - Unprocessed arguments array
|
|
640
|
+
|
|
641
|
+
// Call original but also add test logging
|
|
642
|
+
console.log('Analytics called:', context.rawArgs);
|
|
643
|
+
context.original.apply(context.self, context.rawArgs);
|
|
644
|
+
}).args('event', 'data'));
|
|
645
|
+
|
|
646
|
+
analytics.track('page_view', { page: '/home' });
|
|
647
|
+
expect(spy.calls(0)).to.deep.equal({
|
|
648
|
+
event: 'page_view',
|
|
649
|
+
data: { page: '/home' }
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
spy.restore();
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
**Spy on class prototypes (affects all instances):**
|
|
656
|
+
|
|
657
|
+
```javascript
|
|
658
|
+
class HttpClient {
|
|
659
|
+
request(url, options) {
|
|
660
|
+
return fetch(url, options);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const spy = mocky.spy(HttpClient.prototype, 'request');
|
|
665
|
+
|
|
666
|
+
const client1 = new HttpClient();
|
|
667
|
+
const client2 = new HttpClient();
|
|
668
|
+
|
|
669
|
+
client1.request('/api/users', { method: 'GET' });
|
|
670
|
+
client2.request('/api/posts', { method: 'GET' });
|
|
671
|
+
|
|
672
|
+
// Both instances' calls are tracked
|
|
673
|
+
expect(spy.calls().length).to.equal(2);
|
|
674
|
+
spy.restore();
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
## ð Coming from Jest?
|
|
680
|
+
|
|
681
|
+
Migration guide for Jest users:
|
|
682
|
+
|
|
683
|
+
| Jest | lil-mocky |
|
|
684
|
+
|------|-----------|
|
|
685
|
+
| `jest.fn()` | `mocky.function().build()` |
|
|
686
|
+
| `mock.mockReturnValue(val)` | `mock.ret(val)` |
|
|
687
|
+
| `mock.mock.calls[0][0]` | `mock.calls(0)` |
|
|
688
|
+
| `jest.spyOn(obj, 'method')` | `mocky.spy(obj, 'method')` |
|
|
689
|
+
| `spy.mockRestore()` | `spy.restore()` |
|
|
690
|
+
|
|
691
|
+
**Key differences:**
|
|
692
|
+
- **Builder pattern**: Explicit configuration via chainable builders before `.build()`
|
|
693
|
+
- **Named arguments**: Built-in support for named argument tracking with `.args()`
|
|
694
|
+
- **Per-instance class mocks**: Configure different behavior for each class instance
|
|
695
|
+
- **Simpler API**: Fewer concepts, more predictable behavior
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## ð License
|
|
700
|
+
|
|
701
|
+
MIT
|
|
702
|
+
|
|
703
|
+
## ðĪ Contributing
|
|
704
|
+
|
|
705
|
+
Contributions welcome! Please open an issue or PR on GitHub.
|