From d01392df855e2202bd817ba6812fa3d3ffb3286f Mon Sep 17 00:00:00 2001 From: UselessPickles Date: Wed, 7 Mar 2018 19:24:24 -0500 Subject: [PATCH 1/2] Enhance function mocks to maintain a list of returned values. --- docs/MockFunctionAPI.md | 15 ++- docs/MockFunctions.md | 13 ++- .../jest-mock/src/__tests__/jest_mock.test.js | 38 +++++++ packages/jest-mock/src/index.js | 101 ++++++++++-------- 4 files changed, 119 insertions(+), 48 deletions(-) diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index 38d9b7f9236f..bdd383089a18 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -29,13 +29,26 @@ Each call is represented by an array of arguments that were passed during the call. For example: A mock function `f` that has been called twice, with the arguments -`f('arg1', 'arg2')`, and then with the arguments `f('arg3', 'arg4')` would have +`f('arg1', 'arg2')`, and then with the arguments `f('arg3', 'arg4')`, would have a `mock.calls` array that looks like this: ```js [['arg1', 'arg2'], ['arg3', 'arg4']]; ``` +### `mockFn.mock.returnValues` + +An array containing values that have been returned by all calls to this mock +function. + +For example: A mock function `f` that has been called twice, returning +`result1`, and then returning `result2`, would have a `mock.returnValues` array +that looks like this: + +```js +['result1', 'result2']; +``` + ### `mockFn.mock.instances` An array that contains all the object instances that have been instantiated from diff --git a/docs/MockFunctions.md b/docs/MockFunctions.md index 6407db8d2510..8d648cb67645 100644 --- a/docs/MockFunctions.md +++ b/docs/MockFunctions.md @@ -41,13 +41,17 @@ expect(mockCallback.mock.calls[0][0]).toBe(0); // The first argument of the second call to the function was 1 expect(mockCallback.mock.calls[1][0]).toBe(1); + +// The return value of the first call to the function was 42 +expect(mockCallback.mock.returnValues[0]).toBe(42); ``` ## `.mock` property All mock functions have this special `.mock` property, which is where data about -how the function has been called is kept. The `.mock` property also tracks the -value of `this` for each call, so it is possible to inspect this as well: +how the function has been called and what the function returned is kept. The +`.mock` property also tracks the value of `this` for each call, so it is +possible to inspect this as well: ```javascript const myMock = jest.fn(); @@ -62,7 +66,7 @@ console.log(myMock.mock.instances); ``` These mock members are very useful in tests to assert how these functions get -called, or instantiated: +called, instantiated, or what they returned: ```javascript // The function was called exactly once @@ -74,6 +78,9 @@ expect(someMockFunction.mock.calls[0][0]).toBe('first arg'); // The second arg of the first call to the function was 'second arg' expect(someMockFunction.mock.calls[0][1]).toBe('second arg'); +// The return value of the first call to the function was 'return value' +expect(someMockFunction.mock.returnValues[0]).toBe('return value'); + // This function was instantiated exactly twice expect(someMockFunction.mock.instances.length).toBe(2); diff --git a/packages/jest-mock/src/__tests__/jest_mock.test.js b/packages/jest-mock/src/__tests__/jest_mock.test.js index ee692bc7efc2..f573e5c31f0f 100644 --- a/packages/jest-mock/src/__tests__/jest_mock.test.js +++ b/packages/jest-mock/src/__tests__/jest_mock.test.js @@ -438,6 +438,44 @@ describe('moduleMocker', () => { ]); }); + describe('return values', () => { + it('tracks return values', () => { + const fn = moduleMocker.fn(x => x * 2); + + expect(fn.mock.returnValues).toEqual([]); + + fn(1); + fn(2); + + expect(fn.mock.returnValues).toEqual([2, 4]); + }); + + it('tracks mocked return values', () => { + const fn = moduleMocker.fn(x => x * 2); + fn.mockReturnValueOnce('MOCKED!'); + + fn(1); + fn(2); + + expect(fn.mock.returnValues).toEqual(['MOCKED!', 4]); + }); + + it('supports resetting return values', () => { + const fn = moduleMocker.fn(x => x * 2); + + expect(fn.mock.returnValues).toEqual([]); + + fn(1); + fn(2); + + expect(fn.mock.returnValues).toEqual([2, 4]); + + fn.mockReset(); + + expect(fn.mock.returnValues).toEqual([]); + }); + }); + describe('timestamps', () => { const RealDate = Date; diff --git a/packages/jest-mock/src/index.js b/packages/jest-mock/src/index.js index b62d81fae5fa..10113434233c 100644 --- a/packages/jest-mock/src/index.js +++ b/packages/jest-mock/src/index.js @@ -24,6 +24,7 @@ export type MockFunctionMetadata = { type MockFunctionState = { instances: Array, calls: Array>, + returnValues: Array, timestamps: Array, }; @@ -280,6 +281,7 @@ class ModuleMockerClass { return { calls: [], instances: [], + returnValues: [], timestamps: [], }; } @@ -316,58 +318,69 @@ class ModuleMockerClass { mockState.instances.push(this); mockState.calls.push(Array.prototype.slice.call(arguments)); mockState.timestamps.push(Date.now()); - if (this instanceof f) { - // This is probably being called as a constructor - prototypeSlots.forEach(slot => { - // Copy prototype methods to the instance to make - // it easier to interact with mock instance call and - // return values - if (prototype[slot].type === 'function') { - const protoImpl = this[slot]; - this[slot] = mocker.generateFromMetadata(prototype[slot]); - this[slot]._protoImpl = protoImpl; - } - }); - // Run the mock constructor implementation - const mockImpl = mockConfig.specificMockImpls.length - ? mockConfig.specificMockImpls.shift() - : mockConfig.mockImpl; - return mockImpl && mockImpl.apply(this, arguments); - } + // The bulk of the implementation is wrapped in an immediately executed + // arrow function so the return value of the mock function can + // be easily captured and recorded, despite the many separate return + // points within the logic. + const finalReturnValue = (() => { + if (this instanceof f) { + // This is probably being called as a constructor + prototypeSlots.forEach(slot => { + // Copy prototype methods to the instance to make + // it easier to interact with mock instance call and + // return values + if (prototype[slot].type === 'function') { + const protoImpl = this[slot]; + this[slot] = mocker.generateFromMetadata(prototype[slot]); + this[slot]._protoImpl = protoImpl; + } + }); - const returnValue = mockConfig.defaultReturnValue; - // If return value is last set, either specific or default, i.e. - // mockReturnValueOnce()/mockReturnValue() is called and no - // mockImplementationOnce()/mockImplementation() is called after that. - // use the set return value. - if (mockConfig.specificReturnValues.length) { - return mockConfig.specificReturnValues.shift(); - } + // Run the mock constructor implementation + const mockImpl = mockConfig.specificMockImpls.length + ? mockConfig.specificMockImpls.shift() + : mockConfig.mockImpl; + return mockImpl && mockImpl.apply(this, arguments); + } - if (mockConfig.isReturnValueLastSet) { - return mockConfig.defaultReturnValue; - } + const returnValue = mockConfig.defaultReturnValue; + // If return value is last set, either specific or default, i.e. + // mockReturnValueOnce()/mockReturnValue() is called and no + // mockImplementationOnce()/mockImplementation() is called after that. + // use the set return value. + if (mockConfig.specificReturnValues.length) { + return mockConfig.specificReturnValues.shift(); + } - // If mockImplementationOnce()/mockImplementation() is last set, - // or specific return values are used up, use the mock implementation. - let specificMockImpl; - if (returnValue === undefined) { - specificMockImpl = mockConfig.specificMockImpls.shift(); - if (specificMockImpl === undefined) { - specificMockImpl = mockConfig.mockImpl; + if (mockConfig.isReturnValueLastSet) { + return mockConfig.defaultReturnValue; } - if (specificMockImpl) { - return specificMockImpl.apply(this, arguments); + + // If mockImplementationOnce()/mockImplementation() is last set, + // or specific return values are used up, use the mock implementation. + let specificMockImpl; + if (returnValue === undefined) { + specificMockImpl = mockConfig.specificMockImpls.shift(); + if (specificMockImpl === undefined) { + specificMockImpl = mockConfig.mockImpl; + } + if (specificMockImpl) { + return specificMockImpl.apply(this, arguments); + } } - } - // Otherwise use prototype implementation - if (returnValue === undefined && f._protoImpl) { - return f._protoImpl.apply(this, arguments); - } + // Otherwise use prototype implementation + if (returnValue === undefined && f._protoImpl) { + return f._protoImpl.apply(this, arguments); + } + + return returnValue; + })(); - return returnValue; + // Record the return value of the mock function before returning it. + mockState.returnValues.push(finalReturnValue); + return finalReturnValue; }, metadata.length || 0); f = this._createMockFunction(metadata, mockConstructor); From 7928d75b918b0d1233a87dfffba2cd1afe78b9b1 Mon Sep 17 00:00:00 2001 From: UselessPickles Date: Wed, 7 Mar 2018 20:03:54 -0500 Subject: [PATCH 2/2] Update CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79bf4ef86472..c2189bf38d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ ([#5670](https://github.com/facebook/jest/pull/5670)) * `[expect]` Add inverse matchers (`expect.not.arrayContaining`, etc., [#5517](https://github.com/facebook/jest/pull/5517)) +* `[jest-mock]` Add tracking of return values in the `mock` property + ([#5738](https://github.com/facebook/jest/issues/5738)) ### Fixes