Skip to content

Commit 77d6fec

Browse files
UselessPicklescpojer
authored andcommitted
Enhance function mocks to expose a list of returned values. (#5752)
* Enhance function mocks to maintain a list of returned values. * Update CHANGELOG
1 parent 55daf31 commit 77d6fec

5 files changed

Lines changed: 121 additions & 48 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
([#5670](https://github.com/facebook/jest/pull/5670))
1717
* `[expect]` Add inverse matchers (`expect.not.arrayContaining`, etc.,
1818
[#5517](https://github.com/facebook/jest/pull/5517))
19+
* `[jest-mock]` Add tracking of return values in the `mock` property
20+
([#5738](https://github.com/facebook/jest/issues/5738))
1921
* `[expect]`Add nthCalledWith spy matcher
2022
([#5605](https://github.com/facebook/jest/pull/5605))
2123
* `[jest-cli]` Add `isSerial` property that runners can expose to specify that

docs/MockFunctionAPI.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,26 @@ Each call is represented by an array of arguments that were passed during the
2929
call.
3030

3131
For example: A mock function `f` that has been called twice, with the arguments
32-
`f('arg1', 'arg2')`, and then with the arguments `f('arg3', 'arg4')` would have
32+
`f('arg1', 'arg2')`, and then with the arguments `f('arg3', 'arg4')`, would have
3333
a `mock.calls` array that looks like this:
3434

3535
```js
3636
[['arg1', 'arg2'], ['arg3', 'arg4']];
3737
```
3838

39+
### `mockFn.mock.returnValues`
40+
41+
An array containing values that have been returned by all calls to this mock
42+
function.
43+
44+
For example: A mock function `f` that has been called twice, returning
45+
`result1`, and then returning `result2`, would have a `mock.returnValues` array
46+
that looks like this:
47+
48+
```js
49+
['result1', 'result2'];
50+
```
51+
3952
### `mockFn.mock.instances`
4053

4154
An array that contains all the object instances that have been instantiated from

docs/MockFunctions.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,17 @@ expect(mockCallback.mock.calls[0][0]).toBe(0);
4141

4242
// The first argument of the second call to the function was 1
4343
expect(mockCallback.mock.calls[1][0]).toBe(1);
44+
45+
// The return value of the first call to the function was 42
46+
expect(mockCallback.mock.returnValues[0]).toBe(42);
4447
```
4548

4649
## `.mock` property
4750

4851
All mock functions have this special `.mock` property, which is where data about
49-
how the function has been called is kept. The `.mock` property also tracks the
50-
value of `this` for each call, so it is possible to inspect this as well:
52+
how the function has been called and what the function returned is kept. The
53+
`.mock` property also tracks the value of `this` for each call, so it is
54+
possible to inspect this as well:
5155

5256
```javascript
5357
const myMock = jest.fn();
@@ -62,7 +66,7 @@ console.log(myMock.mock.instances);
6266
```
6367

6468
These mock members are very useful in tests to assert how these functions get
65-
called, or instantiated:
69+
called, instantiated, or what they returned:
6670

6771
```javascript
6872
// The function was called exactly once
@@ -74,6 +78,9 @@ expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
7478
// The second arg of the first call to the function was 'second arg'
7579
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');
7680

81+
// The return value of the first call to the function was 'return value'
82+
expect(someMockFunction.mock.returnValues[0]).toBe('return value');
83+
7784
// This function was instantiated exactly twice
7885
expect(someMockFunction.mock.instances.length).toBe(2);
7986

packages/jest-mock/src/__tests__/jest_mock.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,44 @@ describe('moduleMocker', () => {
438438
]);
439439
});
440440

441+
describe('return values', () => {
442+
it('tracks return values', () => {
443+
const fn = moduleMocker.fn(x => x * 2);
444+
445+
expect(fn.mock.returnValues).toEqual([]);
446+
447+
fn(1);
448+
fn(2);
449+
450+
expect(fn.mock.returnValues).toEqual([2, 4]);
451+
});
452+
453+
it('tracks mocked return values', () => {
454+
const fn = moduleMocker.fn(x => x * 2);
455+
fn.mockReturnValueOnce('MOCKED!');
456+
457+
fn(1);
458+
fn(2);
459+
460+
expect(fn.mock.returnValues).toEqual(['MOCKED!', 4]);
461+
});
462+
463+
it('supports resetting return values', () => {
464+
const fn = moduleMocker.fn(x => x * 2);
465+
466+
expect(fn.mock.returnValues).toEqual([]);
467+
468+
fn(1);
469+
fn(2);
470+
471+
expect(fn.mock.returnValues).toEqual([2, 4]);
472+
473+
fn.mockReset();
474+
475+
expect(fn.mock.returnValues).toEqual([]);
476+
});
477+
});
478+
441479
describe('timestamps', () => {
442480
const RealDate = Date;
443481

packages/jest-mock/src/index.js

Lines changed: 57 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export type MockFunctionMetadata = {
2424
type MockFunctionState = {
2525
instances: Array<any>,
2626
calls: Array<Array<any>>,
27+
returnValues: Array<any>,
2728
timestamps: Array<number>,
2829
};
2930

@@ -280,6 +281,7 @@ class ModuleMockerClass {
280281
return {
281282
calls: [],
282283
instances: [],
284+
returnValues: [],
283285
timestamps: [],
284286
};
285287
}
@@ -316,58 +318,69 @@ class ModuleMockerClass {
316318
mockState.instances.push(this);
317319
mockState.calls.push(Array.prototype.slice.call(arguments));
318320
mockState.timestamps.push(Date.now());
319-
if (this instanceof f) {
320-
// This is probably being called as a constructor
321-
prototypeSlots.forEach(slot => {
322-
// Copy prototype methods to the instance to make
323-
// it easier to interact with mock instance call and
324-
// return values
325-
if (prototype[slot].type === 'function') {
326-
const protoImpl = this[slot];
327-
this[slot] = mocker.generateFromMetadata(prototype[slot]);
328-
this[slot]._protoImpl = protoImpl;
329-
}
330-
});
331321

332-
// Run the mock constructor implementation
333-
const mockImpl = mockConfig.specificMockImpls.length
334-
? mockConfig.specificMockImpls.shift()
335-
: mockConfig.mockImpl;
336-
return mockImpl && mockImpl.apply(this, arguments);
337-
}
322+
// The bulk of the implementation is wrapped in an immediately executed
323+
// arrow function so the return value of the mock function can
324+
// be easily captured and recorded, despite the many separate return
325+
// points within the logic.
326+
const finalReturnValue = (() => {
327+
if (this instanceof f) {
328+
// This is probably being called as a constructor
329+
prototypeSlots.forEach(slot => {
330+
// Copy prototype methods to the instance to make
331+
// it easier to interact with mock instance call and
332+
// return values
333+
if (prototype[slot].type === 'function') {
334+
const protoImpl = this[slot];
335+
this[slot] = mocker.generateFromMetadata(prototype[slot]);
336+
this[slot]._protoImpl = protoImpl;
337+
}
338+
});
338339

339-
const returnValue = mockConfig.defaultReturnValue;
340-
// If return value is last set, either specific or default, i.e.
341-
// mockReturnValueOnce()/mockReturnValue() is called and no
342-
// mockImplementationOnce()/mockImplementation() is called after that.
343-
// use the set return value.
344-
if (mockConfig.specificReturnValues.length) {
345-
return mockConfig.specificReturnValues.shift();
346-
}
340+
// Run the mock constructor implementation
341+
const mockImpl = mockConfig.specificMockImpls.length
342+
? mockConfig.specificMockImpls.shift()
343+
: mockConfig.mockImpl;
344+
return mockImpl && mockImpl.apply(this, arguments);
345+
}
347346

348-
if (mockConfig.isReturnValueLastSet) {
349-
return mockConfig.defaultReturnValue;
350-
}
347+
const returnValue = mockConfig.defaultReturnValue;
348+
// If return value is last set, either specific or default, i.e.
349+
// mockReturnValueOnce()/mockReturnValue() is called and no
350+
// mockImplementationOnce()/mockImplementation() is called after that.
351+
// use the set return value.
352+
if (mockConfig.specificReturnValues.length) {
353+
return mockConfig.specificReturnValues.shift();
354+
}
351355

352-
// If mockImplementationOnce()/mockImplementation() is last set,
353-
// or specific return values are used up, use the mock implementation.
354-
let specificMockImpl;
355-
if (returnValue === undefined) {
356-
specificMockImpl = mockConfig.specificMockImpls.shift();
357-
if (specificMockImpl === undefined) {
358-
specificMockImpl = mockConfig.mockImpl;
356+
if (mockConfig.isReturnValueLastSet) {
357+
return mockConfig.defaultReturnValue;
359358
}
360-
if (specificMockImpl) {
361-
return specificMockImpl.apply(this, arguments);
359+
360+
// If mockImplementationOnce()/mockImplementation() is last set,
361+
// or specific return values are used up, use the mock implementation.
362+
let specificMockImpl;
363+
if (returnValue === undefined) {
364+
specificMockImpl = mockConfig.specificMockImpls.shift();
365+
if (specificMockImpl === undefined) {
366+
specificMockImpl = mockConfig.mockImpl;
367+
}
368+
if (specificMockImpl) {
369+
return specificMockImpl.apply(this, arguments);
370+
}
362371
}
363-
}
364372

365-
// Otherwise use prototype implementation
366-
if (returnValue === undefined && f._protoImpl) {
367-
return f._protoImpl.apply(this, arguments);
368-
}
373+
// Otherwise use prototype implementation
374+
if (returnValue === undefined && f._protoImpl) {
375+
return f._protoImpl.apply(this, arguments);
376+
}
377+
378+
return returnValue;
379+
})();
369380

370-
return returnValue;
381+
// Record the return value of the mock function before returning it.
382+
mockState.returnValues.push(finalReturnValue);
383+
return finalReturnValue;
371384
}, metadata.length || 0);
372385

373386
f = this._createMockFunction(metadata, mockConstructor);

0 commit comments

Comments
 (0)