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
|
@@ -0,0 +1,867 @@
|
|
|
1
|
+
const { expect } = require('chai');
|
|
2
|
+
const mocky = require('../src/lil-mocky.js');
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
describe('lil-mocky', () => {
|
|
6
|
+
describe('function', () => {
|
|
7
|
+
it('will build directly without mocky.create', async () => {
|
|
8
|
+
const mock = mocky.function().args('x', 'y').build();
|
|
9
|
+
mock.ret('result');
|
|
10
|
+
|
|
11
|
+
expect(mock(1, 2)).to.equal('result');
|
|
12
|
+
expect(mock.calls(0)).to.deep.equal({ x: 1, y: 2 });
|
|
13
|
+
});
|
|
14
|
+
it('will be a function with args', async () => {
|
|
15
|
+
const mock = mocky.create(mocky.function().args('firstArg', { secondArg: 'testDefault' }));
|
|
16
|
+
mock.ret('testRet');
|
|
17
|
+
|
|
18
|
+
expect(mock('testArg')).to.equal('testRet');
|
|
19
|
+
expect(mock.calls(0)).to.deep.equal({
|
|
20
|
+
firstArg: 'testArg',
|
|
21
|
+
secondArg: 'testDefault'
|
|
22
|
+
});
|
|
23
|
+
expect(mock.calls().length).to.equal(1);
|
|
24
|
+
});
|
|
25
|
+
it('will be a function with custom return', async () => {
|
|
26
|
+
const mock = mocky.create(mocky.function((context) => {
|
|
27
|
+
return context.ret;
|
|
28
|
+
}).args('firstArg'));
|
|
29
|
+
mock.ret('testRet');
|
|
30
|
+
|
|
31
|
+
expect(mock('testArg')).to.equal('testRet');
|
|
32
|
+
expect(mock.calls(0)).to.deep.equal({ firstArg: 'testArg' });
|
|
33
|
+
expect(mock.calls().length).to.equal(1);
|
|
34
|
+
});
|
|
35
|
+
it('will use argSelect with single index to return raw argument', async () => {
|
|
36
|
+
const mock = mocky.create(mocky.function().argSelect(1));
|
|
37
|
+
mock.ret('testRet');
|
|
38
|
+
|
|
39
|
+
const result = mock('first', { test: 'data' }, 'third');
|
|
40
|
+
|
|
41
|
+
expect(result).to.equal('testRet');
|
|
42
|
+
// When argSelect has single index, calls returns the raw argument
|
|
43
|
+
expect(mock.calls(0)).to.deep.equal({ test: 'data' });
|
|
44
|
+
});
|
|
45
|
+
it('will use argSelect with multiple indexes to filter named args', async () => {
|
|
46
|
+
const mock = mocky.create(mocky.function().args('first', 'second', 'third').argSelect(0, 2));
|
|
47
|
+
|
|
48
|
+
mock('value1', 'value2', 'value3');
|
|
49
|
+
|
|
50
|
+
// Only first and third are captured
|
|
51
|
+
expect(mock.calls(0)).to.deep.equal({
|
|
52
|
+
first: 'value1',
|
|
53
|
+
third: 'value3'
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
it('will deep clone arguments to prevent mutation', async () => {
|
|
57
|
+
const mock = mocky.create(mocky.function().args('data'));
|
|
58
|
+
|
|
59
|
+
const obj = { nested: { value: 'original' } };
|
|
60
|
+
mock(obj);
|
|
61
|
+
|
|
62
|
+
// Mutate the original
|
|
63
|
+
obj.nested.value = 'mutated';
|
|
64
|
+
|
|
65
|
+
// Stored call should be unchanged
|
|
66
|
+
expect(mock.calls(0).data.nested.value).to.equal('original');
|
|
67
|
+
});
|
|
68
|
+
it('will reset function state including calls, returns and data', async () => {
|
|
69
|
+
const mock = mocky.create(mocky.function((context) => {
|
|
70
|
+
context.state.data.counter = (context.state.data.counter || 0) + 1;
|
|
71
|
+
return context.ret;
|
|
72
|
+
}).args('arg'));
|
|
73
|
+
mock.ret('test-ret');
|
|
74
|
+
|
|
75
|
+
mock('call1');
|
|
76
|
+
mock('call2');
|
|
77
|
+
|
|
78
|
+
expect(mock.calls().length).to.equal(2);
|
|
79
|
+
expect(mock.data('counter')).to.equal(2);
|
|
80
|
+
|
|
81
|
+
mock.reset();
|
|
82
|
+
|
|
83
|
+
expect(mock.calls().length).to.equal(0);
|
|
84
|
+
expect(mock('call3')).to.equal(undefined); // Return values cleared
|
|
85
|
+
expect(mock.data('counter')).to.equal(1); // Restarted from scratch
|
|
86
|
+
expect(mock.data()).to.deep.equal({ counter: 1 });
|
|
87
|
+
});
|
|
88
|
+
it('will provide context with call, args, ret, state and data', async () => {
|
|
89
|
+
const mock = mocky.create(mocky.function((context) => {
|
|
90
|
+
context.state.data.allItems = context.state.data.allItems || [];
|
|
91
|
+
context.state.data.allItems.push(...context.args.items);
|
|
92
|
+
return {
|
|
93
|
+
call: context.call,
|
|
94
|
+
args: context.args,
|
|
95
|
+
ret: context.ret,
|
|
96
|
+
hasState: typeof context.state === 'object',
|
|
97
|
+
hasData: typeof context.state.data === 'object'
|
|
98
|
+
};
|
|
99
|
+
}).args('items'));
|
|
100
|
+
mock.ret('custom-ret');
|
|
101
|
+
|
|
102
|
+
const result = mock(['a', 'b']);
|
|
103
|
+
|
|
104
|
+
expect(result.call).to.equal(1);
|
|
105
|
+
expect(result.args).to.deep.equal({ items: ['a', 'b'] });
|
|
106
|
+
expect(result.ret).to.equal('custom-ret');
|
|
107
|
+
expect(result.hasState).to.equal(true);
|
|
108
|
+
expect(result.hasData).to.equal(true);
|
|
109
|
+
|
|
110
|
+
// Data accumulates across calls
|
|
111
|
+
mock(['c']);
|
|
112
|
+
expect(mock.data('allItems')).to.deep.equal(['a', 'b', 'c']);
|
|
113
|
+
});
|
|
114
|
+
it('will be async function', async () => {
|
|
115
|
+
const mock = mocky.function().args('x').async().build();
|
|
116
|
+
mock.ret('async-result');
|
|
117
|
+
|
|
118
|
+
const result = mock('test');
|
|
119
|
+
|
|
120
|
+
expect(result).to.be.a('promise');
|
|
121
|
+
expect(await result).to.equal('async-result');
|
|
122
|
+
expect(mock.calls(0)).to.deep.equal({ x: 'test' });
|
|
123
|
+
});
|
|
124
|
+
it('will return different values per call index', () => {
|
|
125
|
+
const mock = mocky.function().build();
|
|
126
|
+
|
|
127
|
+
mock.ret('default');
|
|
128
|
+
mock.ret('first', 1);
|
|
129
|
+
mock.ret('second', 2);
|
|
130
|
+
|
|
131
|
+
expect(mock()).to.equal('first');
|
|
132
|
+
expect(mock()).to.equal('second');
|
|
133
|
+
expect(mock()).to.equal('default');
|
|
134
|
+
expect(mock()).to.equal('default');
|
|
135
|
+
});
|
|
136
|
+
it('will use onRet handler for return value', () => {
|
|
137
|
+
const mock = mocky.function().args('x', 'y').build();
|
|
138
|
+
|
|
139
|
+
mock.onRet((args) => args.x + args.y);
|
|
140
|
+
|
|
141
|
+
expect(mock(2, 3)).to.equal(5);
|
|
142
|
+
expect(mock(10, 20)).to.equal(30);
|
|
143
|
+
});
|
|
144
|
+
it('will use onRet handler per call index', () => {
|
|
145
|
+
const mock = mocky.function().args('x').build();
|
|
146
|
+
|
|
147
|
+
mock.onRet((args) => args.x * 10);
|
|
148
|
+
mock.onRet((args) => args.x * 100, 1);
|
|
149
|
+
|
|
150
|
+
expect(mock(5)).to.equal(500); // call 1 handler
|
|
151
|
+
expect(mock(5)).to.equal(50); // default handler
|
|
152
|
+
});
|
|
153
|
+
it('will throw error when ret is an Error', () => {
|
|
154
|
+
const mock = mocky.function().build();
|
|
155
|
+
|
|
156
|
+
mock.ret(new Error('test error'));
|
|
157
|
+
|
|
158
|
+
expect(() => mock()).to.throw('test error');
|
|
159
|
+
expect(mock.calls().length).to.equal(1);
|
|
160
|
+
});
|
|
161
|
+
it('will provide rawArgs in context', () => {
|
|
162
|
+
const mock = mocky.function((context) => {
|
|
163
|
+
return context.rawArgs;
|
|
164
|
+
}).args('first').build();
|
|
165
|
+
|
|
166
|
+
const result = mock('a', 'b', 'c');
|
|
167
|
+
|
|
168
|
+
expect(result).to.deep.equal(['a', 'b', 'c']);
|
|
169
|
+
});
|
|
170
|
+
it('will provide original function in context', () => {
|
|
171
|
+
const original = (x) => x * 2;
|
|
172
|
+
const mock = mocky.function((context) => {
|
|
173
|
+
return context.original(context.args.x) + 100;
|
|
174
|
+
}).args('x').original(original).build();
|
|
175
|
+
|
|
176
|
+
expect(mock(5)).to.equal(110); // (5 * 2) + 100
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('object', () => {
|
|
181
|
+
it('will be object with function', async () => {
|
|
182
|
+
const mock = mocky.create(mocky.object({
|
|
183
|
+
run: mocky.function().args('firstArg')
|
|
184
|
+
}));
|
|
185
|
+
|
|
186
|
+
mock.run.ret('testRet');
|
|
187
|
+
expect(mock.run('testArg')).to.equal('testRet');
|
|
188
|
+
expect(mock.run.calls(0)).to.deep.equal({ firstArg: 'testArg' });
|
|
189
|
+
expect(mock.run.calls().length).to.equal(1);
|
|
190
|
+
});
|
|
191
|
+
it('will be object with nested object and function', async () => {
|
|
192
|
+
const mock = mocky.create(mocky.object({
|
|
193
|
+
sub: mocky.object({
|
|
194
|
+
run: mocky.function().args('firstArg')
|
|
195
|
+
})
|
|
196
|
+
}));
|
|
197
|
+
mock.sub.run.ret('testRet');
|
|
198
|
+
|
|
199
|
+
expect(mock.sub.run('testArg')).to.equal('testRet');
|
|
200
|
+
expect(mock.sub.run.calls(0)).to.deep.equal({ firstArg: 'testArg' });
|
|
201
|
+
expect(mock.sub.run.calls().length).to.equal(1);
|
|
202
|
+
});
|
|
203
|
+
it('will reset all nested mocks when object is reset', async () => {
|
|
204
|
+
const mock = mocky.create(mocky.object({
|
|
205
|
+
method1: mocky.function().args('arg'),
|
|
206
|
+
nested: mocky.object({
|
|
207
|
+
method2: mocky.function().args('arg')
|
|
208
|
+
})
|
|
209
|
+
}));
|
|
210
|
+
|
|
211
|
+
mock.method1.ret('ret1');
|
|
212
|
+
mock.nested.method2.ret('ret2');
|
|
213
|
+
|
|
214
|
+
mock.method1('call1');
|
|
215
|
+
mock.nested.method2('call2');
|
|
216
|
+
|
|
217
|
+
expect(mock.method1.calls().length).to.equal(1);
|
|
218
|
+
expect(mock.nested.method2.calls().length).to.equal(1);
|
|
219
|
+
|
|
220
|
+
// Reset should propagate to all nested mocks
|
|
221
|
+
mock.reset();
|
|
222
|
+
|
|
223
|
+
expect(mock.method1.calls().length).to.equal(0);
|
|
224
|
+
expect(mock.nested.method2.calls().length).to.equal(0);
|
|
225
|
+
expect(mock.method1('test')).to.equal(undefined);
|
|
226
|
+
expect(mock.nested.method2('test')).to.equal(undefined);
|
|
227
|
+
});
|
|
228
|
+
it('will be object with property getter and setter', async () => {
|
|
229
|
+
const mock = mocky.create(mocky.object({
|
|
230
|
+
accessor: mocky.property()
|
|
231
|
+
}));
|
|
232
|
+
|
|
233
|
+
// Initial value should be undefined
|
|
234
|
+
expect(mock.accessor).to.equal(undefined);
|
|
235
|
+
|
|
236
|
+
// Set a value
|
|
237
|
+
mock.accessor = 'testValue';
|
|
238
|
+
expect(mock.accessor).to.equal('testValue');
|
|
239
|
+
|
|
240
|
+
// Change the value
|
|
241
|
+
mock.accessor = 'newValue';
|
|
242
|
+
expect(mock.accessor).to.equal('newValue');
|
|
243
|
+
});
|
|
244
|
+
it('will reset all property types to initial values', async () => {
|
|
245
|
+
const mock = mocky.create(mocky.object({
|
|
246
|
+
counter: 42,
|
|
247
|
+
name: 'Alice',
|
|
248
|
+
method: mocky.function(),
|
|
249
|
+
accessor: mocky.property()
|
|
250
|
+
}));
|
|
251
|
+
|
|
252
|
+
// Modify all properties
|
|
253
|
+
mock.counter = 100;
|
|
254
|
+
mock.name = 'Bob';
|
|
255
|
+
mock.method.ret('test');
|
|
256
|
+
mock.method();
|
|
257
|
+
mock.accessor = 'value';
|
|
258
|
+
|
|
259
|
+
// Add dynamic property
|
|
260
|
+
mock.added = 'new';
|
|
261
|
+
|
|
262
|
+
mock.reset();
|
|
263
|
+
|
|
264
|
+
// Plain values reset
|
|
265
|
+
expect(mock.counter).to.equal(42);
|
|
266
|
+
expect(mock.name).to.equal('Alice');
|
|
267
|
+
|
|
268
|
+
// Methods reset
|
|
269
|
+
expect(mock.method.calls().length).to.equal(0);
|
|
270
|
+
|
|
271
|
+
// mocky.property() resets to undefined
|
|
272
|
+
expect(mock.accessor).to.equal(undefined);
|
|
273
|
+
|
|
274
|
+
// Dynamic properties deleted
|
|
275
|
+
expect(mock.added).to.equal(undefined);
|
|
276
|
+
expect('added' in mock).to.equal(false);
|
|
277
|
+
});
|
|
278
|
+
it('will support Symbol properties and reset them correctly', async () => {
|
|
279
|
+
const customSymbol = Symbol('custom');
|
|
280
|
+
const addedSymbol = Symbol('added');
|
|
281
|
+
|
|
282
|
+
const mock = mocky.create(mocky.object({
|
|
283
|
+
[Symbol.iterator]: mocky.function(),
|
|
284
|
+
[customSymbol]: 'initialValue'
|
|
285
|
+
}));
|
|
286
|
+
|
|
287
|
+
// Verify Symbol properties work
|
|
288
|
+
expect(typeof mock[Symbol.iterator]).to.equal('function');
|
|
289
|
+
expect(mock[customSymbol]).to.equal('initialValue');
|
|
290
|
+
|
|
291
|
+
// Configure and use Symbol function
|
|
292
|
+
mock[Symbol.iterator].ret('iteratorResult');
|
|
293
|
+
expect(mock[Symbol.iterator]()).to.equal('iteratorResult');
|
|
294
|
+
expect(mock[Symbol.iterator].calls().length).to.equal(1);
|
|
295
|
+
|
|
296
|
+
// Modify Symbol value and add dynamic Symbol
|
|
297
|
+
mock[customSymbol] = 'modifiedValue';
|
|
298
|
+
mock[addedSymbol] = 'added';
|
|
299
|
+
|
|
300
|
+
// Reset
|
|
301
|
+
mock.reset();
|
|
302
|
+
|
|
303
|
+
// Symbol function mock was reset
|
|
304
|
+
expect(mock[Symbol.iterator].calls().length).to.equal(0);
|
|
305
|
+
expect(mock[Symbol.iterator]()).to.equal(undefined);
|
|
306
|
+
|
|
307
|
+
// Symbol plain property was restored
|
|
308
|
+
expect(mock[customSymbol]).to.equal('initialValue');
|
|
309
|
+
|
|
310
|
+
// Dynamic Symbol property was deleted
|
|
311
|
+
expect(mock[addedSymbol]).to.equal(undefined);
|
|
312
|
+
expect(Object.getOwnPropertySymbols(mock).includes(addedSymbol)).to.equal(false);
|
|
313
|
+
});
|
|
314
|
+
it('will handle null and undefined property values', async () => {
|
|
315
|
+
const mock = mocky.create(mocky.object({
|
|
316
|
+
nullProp: null,
|
|
317
|
+
undefinedProp: undefined,
|
|
318
|
+
zeroProp: 0,
|
|
319
|
+
falseProp: false,
|
|
320
|
+
emptyStringProp: ''
|
|
321
|
+
}));
|
|
322
|
+
|
|
323
|
+
// Verify initial values
|
|
324
|
+
expect(mock.nullProp).to.equal(null);
|
|
325
|
+
expect(mock.undefinedProp).to.equal(undefined);
|
|
326
|
+
expect(mock.zeroProp).to.equal(0);
|
|
327
|
+
expect(mock.falseProp).to.equal(false);
|
|
328
|
+
expect(mock.emptyStringProp).to.equal('');
|
|
329
|
+
|
|
330
|
+
// Modify values
|
|
331
|
+
mock.nullProp = 'changed';
|
|
332
|
+
mock.undefinedProp = 'changed';
|
|
333
|
+
mock.zeroProp = 100;
|
|
334
|
+
mock.falseProp = true;
|
|
335
|
+
mock.emptyStringProp = 'changed';
|
|
336
|
+
|
|
337
|
+
// Reset
|
|
338
|
+
mock.reset();
|
|
339
|
+
|
|
340
|
+
// Verify restored to initial values
|
|
341
|
+
expect(mock.nullProp).to.equal(null);
|
|
342
|
+
expect(mock.undefinedProp).to.equal(undefined);
|
|
343
|
+
expect(mock.zeroProp).to.equal(0);
|
|
344
|
+
expect(mock.falseProp).to.equal(false);
|
|
345
|
+
expect(mock.emptyStringProp).to.equal('');
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
describe('class', () => {
|
|
350
|
+
it('will be class with constructor and method', async () => {
|
|
351
|
+
const Mock = mocky.create(mocky.class({
|
|
352
|
+
constructor: mocky.function().args('initArg'),
|
|
353
|
+
run: mocky.function().args('firstArg')
|
|
354
|
+
}));
|
|
355
|
+
Mock.inst().run.ret('testRet');
|
|
356
|
+
|
|
357
|
+
const mockInstance = new Mock('testOption');
|
|
358
|
+
expect(Mock.inst().constructor.calls(0)).to.deep.equal({ initArg: 'testOption' });
|
|
359
|
+
expect(Mock.inst().constructor.calls().length).to.equal(1);
|
|
360
|
+
|
|
361
|
+
expect(mockInstance.run('testArg')).to.equal('testRet');
|
|
362
|
+
expect(Mock.inst().run.calls(0)).to.deep.equal({ firstArg: 'testArg' });
|
|
363
|
+
expect(Mock.inst().run.calls().length).to.equal(1);
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('will be class with constructor and builder methods', async () => {
|
|
367
|
+
const Mock = mocky.create(mocky.class({
|
|
368
|
+
index: mocky.function((context) => {
|
|
369
|
+
return context.self;
|
|
370
|
+
}).args('index'),
|
|
371
|
+
namespace: mocky.function((context) => {
|
|
372
|
+
return context.self;
|
|
373
|
+
}).args('namespace'),
|
|
374
|
+
query: mocky.function((context) => {
|
|
375
|
+
return context.self;
|
|
376
|
+
}).args('query')
|
|
377
|
+
}));
|
|
378
|
+
|
|
379
|
+
const mockInstance = new Mock();
|
|
380
|
+
mockInstance.index('test-index').namespace('test-namespace').query({ test: 'query' });
|
|
381
|
+
expect(Mock.inst().index.calls(0)).to.deep.equal({ index: 'test-index' });
|
|
382
|
+
expect(Mock.inst().index.calls().length).to.equal(1);
|
|
383
|
+
expect(Mock.inst().namespace.calls(0)).to.deep.equal({ namespace: 'test-namespace' });
|
|
384
|
+
expect(Mock.inst().namespace.calls().length).to.equal(1);
|
|
385
|
+
expect(Mock.inst().query.calls(0)).to.deep.equal({ query: { test: 'query' } });
|
|
386
|
+
expect(Mock.inst().query.calls().length).to.equal(1);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('will demonstrate that state.data is per-method, not shared', async () => {
|
|
390
|
+
const Counter = mocky.create(mocky.class({
|
|
391
|
+
constructor: mocky.function((context) => {
|
|
392
|
+
// state.data here is separate from other methods
|
|
393
|
+
context.state.data.constructorData = 'set in constructor';
|
|
394
|
+
}).args('startValue'),
|
|
395
|
+
getConstructorData: mocky.function((context) => {
|
|
396
|
+
// This state.data is different from constructor's state.data
|
|
397
|
+
return context.state.data.constructorData; // Will be undefined
|
|
398
|
+
})
|
|
399
|
+
}));
|
|
400
|
+
|
|
401
|
+
const counter = new Counter(10);
|
|
402
|
+
// This will be undefined because each method has its own state.data
|
|
403
|
+
expect(counter.getConstructorData()).to.equal(undefined);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
it('will store data on instance via context.self in constructor', async () => {
|
|
407
|
+
const Counter = mocky.create(mocky.class({
|
|
408
|
+
constructor: mocky.function((context) => {
|
|
409
|
+
// Store data directly on the instance
|
|
410
|
+
context.self._count = context.args.startValue || 0;
|
|
411
|
+
context.self._initialized = true;
|
|
412
|
+
}).args('startValue'),
|
|
413
|
+
increment: mocky.function((context) => {
|
|
414
|
+
context.self._count++;
|
|
415
|
+
return context.self._count;
|
|
416
|
+
}),
|
|
417
|
+
getCount: mocky.function((context) => {
|
|
418
|
+
return context.self._count;
|
|
419
|
+
}),
|
|
420
|
+
isInitialized: mocky.function((context) => {
|
|
421
|
+
return context.self._initialized;
|
|
422
|
+
})
|
|
423
|
+
}));
|
|
424
|
+
|
|
425
|
+
const counter1 = new Counter(10);
|
|
426
|
+
expect(counter1._initialized).to.equal(true);
|
|
427
|
+
expect(counter1.isInitialized()).to.equal(true);
|
|
428
|
+
expect(counter1.getCount()).to.equal(10);
|
|
429
|
+
expect(counter1.increment()).to.equal(11);
|
|
430
|
+
expect(counter1.increment()).to.equal(12);
|
|
431
|
+
expect(counter1.getCount()).to.equal(12);
|
|
432
|
+
|
|
433
|
+
// Second instance should have separate data
|
|
434
|
+
const counter2 = new Counter(5);
|
|
435
|
+
expect(counter2.getCount()).to.equal(5);
|
|
436
|
+
expect(counter2.increment()).to.equal(6);
|
|
437
|
+
|
|
438
|
+
// First instance should be unaffected
|
|
439
|
+
expect(counter1.getCount()).to.equal(12);
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
it('will track number of instances created with numInsts', async () => {
|
|
443
|
+
const Mock = mocky.create(mocky.class({
|
|
444
|
+
constructor: mocky.function().args('value')
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
expect(Mock.numInsts()).to.equal(0);
|
|
448
|
+
|
|
449
|
+
const inst1 = new Mock('first');
|
|
450
|
+
expect(Mock.numInsts()).to.equal(1);
|
|
451
|
+
|
|
452
|
+
const inst2 = new Mock('second');
|
|
453
|
+
expect(Mock.numInsts()).to.equal(2);
|
|
454
|
+
|
|
455
|
+
const inst3 = new Mock('third');
|
|
456
|
+
expect(Mock.numInsts()).to.equal(3);
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
it('will reset all instances and counters', async () => {
|
|
460
|
+
const Mock = mocky.create(mocky.class({
|
|
461
|
+
constructor: mocky.function().args('value'),
|
|
462
|
+
run: mocky.function().args('arg')
|
|
463
|
+
}));
|
|
464
|
+
|
|
465
|
+
Mock.inst(0).run.ret('first-ret');
|
|
466
|
+
Mock.inst(1).run.ret('second-ret');
|
|
467
|
+
|
|
468
|
+
const inst1 = new Mock('first');
|
|
469
|
+
const inst2 = new Mock('second');
|
|
470
|
+
|
|
471
|
+
expect(inst1.run('test')).to.equal('first-ret');
|
|
472
|
+
expect(inst2.run('test')).to.equal('second-ret');
|
|
473
|
+
expect(Mock.numInsts()).to.equal(2);
|
|
474
|
+
expect(Mock.inst(0).run.calls().length).to.equal(1);
|
|
475
|
+
|
|
476
|
+
// Reset should clear everything
|
|
477
|
+
Mock.reset();
|
|
478
|
+
|
|
479
|
+
expect(Mock.numInsts()).to.equal(0);
|
|
480
|
+
|
|
481
|
+
// After reset, new instances start from index 0 again
|
|
482
|
+
const inst3 = new Mock('third');
|
|
483
|
+
expect(Mock.numInsts()).to.equal(1);
|
|
484
|
+
// Returns should be cleared
|
|
485
|
+
expect(inst3.run('test')).to.equal(undefined);
|
|
486
|
+
// Calls should be cleared
|
|
487
|
+
expect(Mock.inst(0).run.calls().length).to.equal(1); // Just this one call
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('will reset plain value properties to initial values', async () => {
|
|
491
|
+
const Mock = mocky.create(mocky.class({
|
|
492
|
+
name: null,
|
|
493
|
+
count: 0
|
|
494
|
+
}));
|
|
495
|
+
|
|
496
|
+
// Create and modify instances
|
|
497
|
+
const inst1 = new Mock();
|
|
498
|
+
inst1.name = 'first';
|
|
499
|
+
inst1.count = 10;
|
|
500
|
+
|
|
501
|
+
const inst2 = new Mock();
|
|
502
|
+
inst2.name = 'second';
|
|
503
|
+
inst2.count = 20;
|
|
504
|
+
|
|
505
|
+
expect(Mock.numInsts()).to.equal(2);
|
|
506
|
+
expect(Mock.inst(0).name).to.equal('first');
|
|
507
|
+
expect(Mock.inst(1).count).to.equal(20);
|
|
508
|
+
|
|
509
|
+
// Reset
|
|
510
|
+
Mock.reset();
|
|
511
|
+
|
|
512
|
+
expect(Mock.numInsts()).to.equal(0);
|
|
513
|
+
|
|
514
|
+
// After reset, descriptions are fresh with initial values
|
|
515
|
+
expect(Mock.inst(0).name).to.equal(null);
|
|
516
|
+
expect(Mock.inst(0).count).to.equal(0);
|
|
517
|
+
|
|
518
|
+
// New instance gets initial values
|
|
519
|
+
const inst3 = new Mock();
|
|
520
|
+
expect(inst3.name).to.equal(null);
|
|
521
|
+
expect(inst3.count).to.equal(0);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('will pre-configure instances before instantiation', async () => {
|
|
525
|
+
const Mock = mocky.create(mocky.class({
|
|
526
|
+
run: mocky.function().args('arg')
|
|
527
|
+
}));
|
|
528
|
+
|
|
529
|
+
// Configure instances before they're created
|
|
530
|
+
Mock.inst(0).run.ret('first-value');
|
|
531
|
+
Mock.inst(1).run.ret('second-value');
|
|
532
|
+
Mock.inst(2).run.ret('third-value');
|
|
533
|
+
|
|
534
|
+
const inst1 = new Mock();
|
|
535
|
+
const inst2 = new Mock();
|
|
536
|
+
const inst3 = new Mock();
|
|
537
|
+
|
|
538
|
+
expect(inst1.run('test')).to.equal('first-value');
|
|
539
|
+
expect(inst2.run('test')).to.equal('second-value');
|
|
540
|
+
expect(inst3.run('test')).to.equal('third-value');
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it('will support async methods', async () => {
|
|
544
|
+
const Mock = mocky.create(mocky.class({
|
|
545
|
+
fetchData: mocky.function().args('id').async()
|
|
546
|
+
}));
|
|
547
|
+
|
|
548
|
+
Mock.inst().fetchData.ret('async-result');
|
|
549
|
+
|
|
550
|
+
const inst = new Mock();
|
|
551
|
+
const result = await inst.fetchData('test-id');
|
|
552
|
+
|
|
553
|
+
expect(result).to.equal('async-result');
|
|
554
|
+
expect(Mock.inst().fetchData.calls(0)).to.deep.equal({ id: 'test-id' });
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('will throw errors from class methods', async () => {
|
|
558
|
+
const Mock = mocky.create(mocky.class({
|
|
559
|
+
throwError: mocky.function().args('message')
|
|
560
|
+
}));
|
|
561
|
+
|
|
562
|
+
const testError = new Error('Test error');
|
|
563
|
+
Mock.inst().throwError.ret(testError);
|
|
564
|
+
|
|
565
|
+
const inst = new Mock();
|
|
566
|
+
|
|
567
|
+
expect(() => inst.throwError('fail')).to.throw('Test error');
|
|
568
|
+
expect(Mock.inst().throwError.calls(0)).to.deep.equal({ message: 'fail' });
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('will handle onRet handlers in class methods', async () => {
|
|
572
|
+
const Mock = mocky.create(mocky.class({
|
|
573
|
+
calculate: mocky.function().args('x', 'y')
|
|
574
|
+
}));
|
|
575
|
+
|
|
576
|
+
Mock.inst().calculate.onRet((args) => {
|
|
577
|
+
return args.x + args.y;
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
const inst = new Mock();
|
|
581
|
+
|
|
582
|
+
expect(inst.calculate(5, 3)).to.equal(8);
|
|
583
|
+
expect(inst.calculate(10, 20)).to.equal(30);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('will handle different return values per call', async () => {
|
|
587
|
+
const Mock = mocky.create(mocky.class({
|
|
588
|
+
getValue: mocky.function()
|
|
589
|
+
}));
|
|
590
|
+
|
|
591
|
+
Mock.inst().getValue.ret('default');
|
|
592
|
+
Mock.inst().getValue.ret('first-call', 1);
|
|
593
|
+
Mock.inst().getValue.ret('second-call', 2);
|
|
594
|
+
|
|
595
|
+
const inst = new Mock();
|
|
596
|
+
|
|
597
|
+
expect(inst.getValue()).to.equal('first-call');
|
|
598
|
+
expect(inst.getValue()).to.equal('second-call');
|
|
599
|
+
expect(inst.getValue()).to.equal('default'); // Falls back to call 0
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it('will support class inheritance with super calls', () => {
|
|
603
|
+
const Mock = mocky.create(mocky.class({
|
|
604
|
+
constructor: mocky.function((context) => {
|
|
605
|
+
context.self.mockInitialized = true;
|
|
606
|
+
context.self.mockValue = context.args.value;
|
|
607
|
+
}).args('value'),
|
|
608
|
+
getValue: mocky.function().args('x')
|
|
609
|
+
}));
|
|
610
|
+
|
|
611
|
+
Mock.inst().getValue.ret('mock-value');
|
|
612
|
+
|
|
613
|
+
class Child extends Mock {
|
|
614
|
+
constructor(value) {
|
|
615
|
+
super(value);
|
|
616
|
+
this.childInitialized = true;
|
|
617
|
+
this.childValue = value * 2;
|
|
618
|
+
}
|
|
619
|
+
getValue(x) {
|
|
620
|
+
const parentResult = super.getValue(x);
|
|
621
|
+
return `child-${parentResult}`;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const child = new Child(10);
|
|
626
|
+
|
|
627
|
+
// Mock constructor should have run
|
|
628
|
+
expect(child.mockInitialized).to.equal(true);
|
|
629
|
+
expect(child.mockValue).to.equal(10);
|
|
630
|
+
expect(Mock.inst().constructor.calls(0)).to.deep.equal({ value: 10 });
|
|
631
|
+
|
|
632
|
+
// Child constructor should have run after
|
|
633
|
+
expect(child.childInitialized).to.equal(true);
|
|
634
|
+
expect(child.childValue).to.equal(20);
|
|
635
|
+
|
|
636
|
+
// Child's overridden method calls super and wraps result
|
|
637
|
+
const result = child.getValue('test');
|
|
638
|
+
expect(result).to.equal('child-mock-value');
|
|
639
|
+
expect(Mock.inst().getValue.calls(0)).to.deep.equal({ x: 'test' });
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it('will not expose __mockyInst in deep equal comparisons', () => {
|
|
643
|
+
const Mock = mocky.create(mocky.class({
|
|
644
|
+
constructor: mocky.function((context) => {
|
|
645
|
+
context.self.name = context.args.name;
|
|
646
|
+
}).args('name')
|
|
647
|
+
}));
|
|
648
|
+
|
|
649
|
+
const instance = new Mock('test');
|
|
650
|
+
|
|
651
|
+
expect(instance).to.deep.equal({ name: 'test' });
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('will sync plain value members between instance and Mock.inst()', () => {
|
|
655
|
+
const Mock = mocky.create(mocky.class({
|
|
656
|
+
name: null,
|
|
657
|
+
count: 0
|
|
658
|
+
}));
|
|
659
|
+
|
|
660
|
+
const inst = new Mock();
|
|
661
|
+
|
|
662
|
+
// Initial values from definition
|
|
663
|
+
expect(inst.name).to.equal(null);
|
|
664
|
+
expect(inst.count).to.equal(0);
|
|
665
|
+
|
|
666
|
+
// Setting on instance syncs to Mock.inst()
|
|
667
|
+
inst.name = 'test';
|
|
668
|
+
inst.count = 42;
|
|
669
|
+
expect(Mock.inst(0).name).to.equal('test');
|
|
670
|
+
expect(Mock.inst(0).count).to.equal(42);
|
|
671
|
+
|
|
672
|
+
// Setting on Mock.inst() syncs to instance
|
|
673
|
+
Mock.inst(0).name = 'changed';
|
|
674
|
+
expect(inst.name).to.equal('changed');
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('will isolate plain value members between class instances', () => {
|
|
678
|
+
const Mock = mocky.create(mocky.class({
|
|
679
|
+
value: null
|
|
680
|
+
}));
|
|
681
|
+
|
|
682
|
+
const inst1 = new Mock();
|
|
683
|
+
const inst2 = new Mock();
|
|
684
|
+
|
|
685
|
+
inst1.value = 'first';
|
|
686
|
+
inst2.value = 'second';
|
|
687
|
+
|
|
688
|
+
expect(inst1.value).to.equal('first');
|
|
689
|
+
expect(inst2.value).to.equal('second');
|
|
690
|
+
expect(Mock.inst(0).value).to.equal('first');
|
|
691
|
+
expect(Mock.inst(1).value).to.equal('second');
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
it('will allow pre-configuring plain value members before instantiation', () => {
|
|
695
|
+
const Mock = mocky.create(mocky.class({
|
|
696
|
+
config: null
|
|
697
|
+
}));
|
|
698
|
+
|
|
699
|
+
// Pre-configure before instantiation
|
|
700
|
+
Mock.inst(0).config = 'preset-value';
|
|
701
|
+
|
|
702
|
+
const inst = new Mock();
|
|
703
|
+
expect(inst.config).to.equal('preset-value');
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
it('will support mixing plain values and methods in class', () => {
|
|
707
|
+
const Mock = mocky.create(mocky.class({
|
|
708
|
+
name: null,
|
|
709
|
+
greet: mocky.function().args('greeting')
|
|
710
|
+
}));
|
|
711
|
+
|
|
712
|
+
Mock.inst().greet.ret('Hello!');
|
|
713
|
+
|
|
714
|
+
const inst = new Mock();
|
|
715
|
+
inst.name = 'World';
|
|
716
|
+
|
|
717
|
+
expect(inst.name).to.equal('World');
|
|
718
|
+
expect(Mock.inst(0).name).to.equal('World');
|
|
719
|
+
expect(inst.greet('Hi')).to.equal('Hello!');
|
|
720
|
+
expect(Mock.inst().greet.calls(0)).to.deep.equal({ greeting: 'Hi' });
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
it('will support nested objects with methods', () => {
|
|
724
|
+
const Mock = mocky.create(mocky.class({
|
|
725
|
+
db: mocky.object({
|
|
726
|
+
query: mocky.function().args('sql'),
|
|
727
|
+
close: mocky.function()
|
|
728
|
+
})
|
|
729
|
+
}));
|
|
730
|
+
|
|
731
|
+
Mock.inst(0).db.query.ret({ rows: ['a', 'b'] });
|
|
732
|
+
Mock.inst(0).db.close.ret(true);
|
|
733
|
+
|
|
734
|
+
const inst = new Mock();
|
|
735
|
+
|
|
736
|
+
expect(inst.db.query('SELECT *')).to.deep.equal({ rows: ['a', 'b'] });
|
|
737
|
+
expect(inst.db.close()).to.equal(true);
|
|
738
|
+
expect(Mock.inst(0).db.query.calls(0)).to.deep.equal({ sql: 'SELECT *' });
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('will access mock helpers directly on instance methods', () => {
|
|
742
|
+
const Mock = mocky.create(mocky.class({
|
|
743
|
+
run: mocky.function().args('arg')
|
|
744
|
+
}));
|
|
745
|
+
|
|
746
|
+
const inst = new Mock();
|
|
747
|
+
|
|
748
|
+
// Configure via instance
|
|
749
|
+
inst.run.ret('test-ret');
|
|
750
|
+
expect(inst.run('hello')).to.equal('test-ret');
|
|
751
|
+
|
|
752
|
+
// Access calls via instance
|
|
753
|
+
expect(inst.run.calls(0)).to.deep.equal({ arg: 'hello' });
|
|
754
|
+
expect(inst.run.calls().length).to.equal(1);
|
|
755
|
+
|
|
756
|
+
// Reset via instance
|
|
757
|
+
inst.run.reset();
|
|
758
|
+
expect(inst.run.calls().length).to.equal(0);
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
describe('spy', () => {
|
|
763
|
+
it('will spy on method, call through, track calls and preserve this', () => {
|
|
764
|
+
const obj = {
|
|
765
|
+
value: 42,
|
|
766
|
+
greet: function(name) {
|
|
767
|
+
return `Hello, ${name}!`;
|
|
768
|
+
},
|
|
769
|
+
getValue: function() {
|
|
770
|
+
return this.value;
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const greetSpy = mocky.spy(obj, 'greet');
|
|
775
|
+
const valueSpy = mocky.spy(obj, 'getValue');
|
|
776
|
+
|
|
777
|
+
// Calls through to original
|
|
778
|
+
expect(obj.greet('World')).to.equal('Hello, World!');
|
|
779
|
+
expect(obj.greet('Test')).to.equal('Hello, Test!');
|
|
780
|
+
|
|
781
|
+
// Tracks multiple calls
|
|
782
|
+
expect(greetSpy.calls().length).to.equal(2);
|
|
783
|
+
expect(greetSpy.calls(0)).to.deep.equal(['World']);
|
|
784
|
+
expect(greetSpy.calls(1)).to.deep.equal(['Test']);
|
|
785
|
+
|
|
786
|
+
// Preserves this context
|
|
787
|
+
expect(obj.getValue()).to.equal(42);
|
|
788
|
+
expect(valueSpy.calls().length).to.equal(1);
|
|
789
|
+
|
|
790
|
+
greetSpy.restore();
|
|
791
|
+
valueSpy.restore();
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
it('will override return value with ret() while still tracking', () => {
|
|
795
|
+
const obj = {
|
|
796
|
+
getValue: function() {
|
|
797
|
+
return 'original';
|
|
798
|
+
}
|
|
799
|
+
};
|
|
800
|
+
|
|
801
|
+
const spy = mocky.spy(obj, 'getValue');
|
|
802
|
+
spy.ret('overridden');
|
|
803
|
+
|
|
804
|
+
const result = obj.getValue();
|
|
805
|
+
|
|
806
|
+
expect(result).to.equal('overridden');
|
|
807
|
+
expect(spy.calls().length).to.equal(1);
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
it('will restore original method', () => {
|
|
811
|
+
const obj = {
|
|
812
|
+
doThing: function() {
|
|
813
|
+
return 'original';
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
const spy = mocky.spy(obj, 'doThing');
|
|
818
|
+
spy.ret('spied');
|
|
819
|
+
|
|
820
|
+
expect(obj.doThing()).to.equal('spied');
|
|
821
|
+
|
|
822
|
+
spy.restore();
|
|
823
|
+
|
|
824
|
+
expect(obj.doThing()).to.equal('original');
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
it('will spy on class prototype method', () => {
|
|
828
|
+
class MyClass {
|
|
829
|
+
greet(name) {
|
|
830
|
+
return `Hello, ${name}`;
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const spy = mocky.spy(MyClass.prototype, 'greet');
|
|
835
|
+
|
|
836
|
+
const instance = new MyClass();
|
|
837
|
+
const result = instance.greet('Test');
|
|
838
|
+
|
|
839
|
+
expect(result).to.equal('Hello, Test');
|
|
840
|
+
expect(spy.calls().length).to.equal(1);
|
|
841
|
+
expect(spy.calls(0)).to.deep.equal(['Test']);
|
|
842
|
+
|
|
843
|
+
spy.restore();
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
it('will spy with replacement function and provide context.original', () => {
|
|
847
|
+
const obj = {
|
|
848
|
+
add: function(a, b) {
|
|
849
|
+
return a + b;
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
// Replacement with args and context.original
|
|
854
|
+
const spy = mocky.spy(obj, 'add', mocky.function((context) => {
|
|
855
|
+
const originalResult = context.original.apply(context.self, context.rawArgs);
|
|
856
|
+
return originalResult * 10;
|
|
857
|
+
}).args('x', 'y'));
|
|
858
|
+
|
|
859
|
+
const result = obj.add(3, 4);
|
|
860
|
+
|
|
861
|
+
expect(result).to.equal(70); // (3 + 4) * 10
|
|
862
|
+
expect(spy.calls(0)).to.deep.equal({ x: 3, y: 4 });
|
|
863
|
+
|
|
864
|
+
spy.restore();
|
|
865
|
+
});
|
|
866
|
+
});
|
|
867
|
+
});
|