diff --git a/CHANGELOG.md b/CHANGELOG.md index f9ff2e28ca13..52f6aad60dce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,9 @@ * `[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)) + ([#5752](https://github.com/facebook/jest/pull/5752)) +* `[jest-mock]` Add tracking of thrown errors in the `mock` property + ([5764](https://github.com/facebook/jest/pull/5764)) * `[expect]`Add nthCalledWith spy matcher ([#5605](https://github.com/facebook/jest/pull/5605)) * `[jest-cli]` Add `isSerial` property that runners can expose to specify that diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index bdd383089a18..cef48922b208 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -39,14 +39,30 @@ a `mock.calls` array that looks like this: ### `mockFn.mock.returnValues` An array containing values that have been returned by all calls to this mock +function. For any call to the mock that throws an error, a value of `undefined` +will be stored in `mock.returnValues`. + +For example: A mock function `f` that has been called three times, returning +`result1`, throwing an error, and then returning `result2`, would have a +`mock.returnValues` array that looks like this: + +```js +['result1', undefined, 'result2']; +``` + +### `mockFn.mock.thrownErrors` + +An array containing errors that have been thrown 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: +For example: A mock function `f` that has been called twice, throwing an +`Error`, and then executing successfully without an error, would have the +following `mock.thrownErrors` array: ```js -['result1', 'result2']; +f.mock.thrownErrors.length === 2; // true +f.mock.thrownErrors[0] instanceof Error; // true +f.mock.thrownErrors[1] === undefined; // true ``` ### `mockFn.mock.instances` diff --git a/packages/jest-mock/src/__tests__/jest_mock.test.js b/packages/jest-mock/src/__tests__/jest_mock.test.js index f573e5c31f0f..58ca77cf74a4 100644 --- a/packages/jest-mock/src/__tests__/jest_mock.test.js +++ b/packages/jest-mock/src/__tests__/jest_mock.test.js @@ -476,6 +476,38 @@ describe('moduleMocker', () => { }); }); + it(`tracks thrown errors without interfering with other tracking`, () => { + const error = new Error('ODD!'); + const fn = moduleMocker.fn((x, y) => { + // multiply params + const result = x * y; + + if (result % 2 === 1) { + // throw error if result is odd + throw error; + } else { + return result; + } + }); + + expect(fn(2, 4)).toBe(8); + + // Mock still throws the error even though it was internally + // caught and recorded + expect(() => { + fn(3, 5); + }).toThrow('ODD!'); + + expect(fn(6, 3)).toBe(18); + + // All call args tracked + expect(fn.mock.calls).toEqual([[2, 4], [3, 5], [6, 3]]); + // tracked return value is undefined when an error is thrown + expect(fn.mock.returnValues).toEqual([8, undefined, 18]); + // tracked thrown error is undefined when an error is NOT thrown + expect(fn.mock.thrownErrors).toEqual([undefined, error, undefined]); + }); + describe('timestamps', () => { const RealDate = Date; diff --git a/packages/jest-mock/src/index.js b/packages/jest-mock/src/index.js index 10113434233c..e6bed9b123e1 100644 --- a/packages/jest-mock/src/index.js +++ b/packages/jest-mock/src/index.js @@ -25,6 +25,7 @@ type MockFunctionState = { instances: Array, calls: Array>, returnValues: Array, + thrownErrors: Array, timestamps: Array, }; @@ -282,6 +283,7 @@ class ModuleMockerClass { calls: [], instances: [], returnValues: [], + thrownErrors: [], timestamps: [], }; } @@ -319,67 +321,85 @@ class ModuleMockerClass { mockState.calls.push(Array.prototype.slice.call(arguments)); mockState.timestamps.push(Date.now()); - // 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; - } - }); - - // Run the mock constructor implementation - const mockImpl = mockConfig.specificMockImpls.length - ? mockConfig.specificMockImpls.shift() - : mockConfig.mockImpl; - return mockImpl && mockImpl.apply(this, arguments); - } - - 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(); - } + // Will be set to the return value of the mock if an error is not thrown + let finalReturnValue; + // Will be set to the error that is thrown by the mock (if it throws) + let thrownError; + + try { + // 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. + 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; + } + }); + + // 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; + })(); + } catch (error) { + // Store the thrown error so we can record it, then re-throw it. + thrownError = error; + throw error; + } finally { + // Record the return value of the mock function. + // If the mock threw an error, then the value will be undefined. + mockState.returnValues.push(finalReturnValue); + // Record the error thrown by the mock function. + // If no error was thrown, then the value will be udnefiend. + mockState.thrownErrors.push(thrownError); + } - // Record the return value of the mock function before returning it. - mockState.returnValues.push(finalReturnValue); return finalReturnValue; }, metadata.length || 0);