diff --git a/CHANGELOG.md b/CHANGELOG.md index 26c1584f6485..8e3511c34134 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -92,6 +92,8 @@ ([#6181](https://github.com/facebook/jest/pull/6181)) * `[expect]` Include custom mock names in error messages ([#6199](https://github.com/facebook/jest/pull/6199)) +* `[expect]` Improve return matchers + ([#6172](https://github.com/facebook/jest/pull/6172)) ### Fixes diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index c818cdbd828c..82ab5a5e7537 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -683,8 +683,9 @@ Note: the nth argument must be positive integer starting from 1. Also under the alias: `.toReturn()` If you have a mock function, you can use `.toHaveReturned` to test that the mock -function returned a value. For example, let's say you have a mock `drink` that -returns `true`. You can write: +function successfully returned (i.e., did not throw an error) at least one time. +For example, let's say you have a mock `drink` that returns `true`. You can +write: ```js test('drinks returns', () => { @@ -700,9 +701,13 @@ test('drinks returns', () => { Also under the alias: `.toReturnTimes(number)` -Use `.toHaveReturnedTimes` to ensure that a mock function returned an exact -number of times. For example, let's say you have a mock `drink` that returns -`true`. You can write: +Use `.toHaveReturnedTimes` to ensure that a mock function returned successfully +(i.e., did not throw an error) an exact number of times. Any calls to the mock +function that throw an error are not counted toward the number of times the +function returned. + +For example, let's say you have a mock `drink` that returns `true`. You can +write: ```js test('drink returns twice', () => { @@ -741,7 +746,9 @@ test('drink returns La Croix', () => { Also under the alias: `.lastReturnedWith(value)` Use `.toHaveLastReturnedWith` to test the specific value that a mock function -last returned. +last returned. If the last call to the mock function threw an error, then this +matcher will fail no matter what value you provided as the expected return +value. For example, let's say you have a mock `drink` that returns the name of the beverage that was consumed. You can write: @@ -764,7 +771,9 @@ test('drink returns La Croix (Orange) last', () => { Also under the alias: `.nthReturnedWith(nthCall, value)` Use `.toHaveNthReturnedWith` to test the specific value that a mock function -returned for the nth call. +returned for the nth call. If the nth call to the mock function threw an error, +then this matcher will fail no matter what value you provided as the expected +return value. For example, let's say you have a mock `drink` that returns the name of the beverage that was consumed. You can write: diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index fd0d83e45a0d..2fd51e5f9767 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -22,9 +22,9 @@ Returns the mock name string set by calling `mockFn.mockName(value)`. ### `mockFn.mock.calls` -An array that represents all calls that have been made into this mock function. -Each call is represented by an array of arguments that were passed during the -call. +An array containing the call arguments of all calls that have been made to this +mock function. Each item in the array is 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 @@ -34,33 +34,35 @@ a `mock.calls` array that looks like this: [['arg1', 'arg2'], ['arg3', 'arg4']]; ``` -### `mockFn.mock.returnValues` +### `mockFn.mock.results` -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`. +An array containing the results of all calls that have been made to this mock +function. Each entry in this array is an object containing a boolean `isThrow` +property, and a `value` property. `isThrow` is true if the call terminated due +to a `throw`, or false if the the call returned normally. The `value` property +contains the value that was thrown or returned. 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: +`mock.results` 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, throwing an -`Error`, and then executing successfully without an error, would have the -following `mock.thrownErrors` array: - -```js -f.mock.thrownErrors.length === 2; // true -f.mock.thrownErrors[0] instanceof Error; // true -f.mock.thrownErrors[1] === undefined; // true +[ + { + isThrow: false, + value: 'result1', + }, + { + isThrow: true, + value: { + /* Error instance */ + }, + }, + { + isThrow: false, + value: 'result2', + }, +]; ``` ### `mockFn.mock.instances` diff --git a/packages/expect/src/__tests__/__snapshots__/spy_matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/spy_matchers.test.js.snap index a1cedf0b33f2..7934e2030ac5 100644 --- a/packages/expect/src/__tests__/__snapshots__/spy_matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/spy_matchers.test.js.snap @@ -125,12 +125,28 @@ Expected mock function to have been last called with: Did not expect argument 2 but it was called with undefined." `; +exports[`lastReturnedWith a call that throws is not considered to have returned 1`] = ` +"expect(jest.fn()).lastReturnedWith(expected) + +Expected mock function to have last returned: + undefined +But the last call threw an error" +`; + +exports[`lastReturnedWith a call that throws undefined is not considered to have returned 1`] = ` +"expect(jest.fn()).lastReturnedWith(expected) + +Expected mock function to have last returned: + undefined +But the last call threw an error" +`; + exports[`lastReturnedWith includes the custom mock name in the error message 1`] = ` "expect(named-mock).lastReturnedWith(expected) Expected mock function \\"named-mock\\" to have last returned: \\"foo\\" -But it did not return" +But it was not called" `; exports[`lastReturnedWith works only on spies or jest.fn 1`] = ` @@ -146,7 +162,7 @@ exports[`lastReturnedWith works when not called 1`] = ` Expected mock function to have last returned: \\"foo\\" -But it did not return" +But it was not called" `; exports[`lastReturnedWith works with Immutable.js objects directly created 1`] = ` @@ -181,7 +197,7 @@ exports[`lastReturnedWith works with Map 2`] = ` Expected mock function to have last returned: Map {\\"a\\" => \\"b\\", \\"b\\" => \\"a\\"} -But it last returned: +But the last call returned: Map {1 => 2, 2 => 1}" `; @@ -199,7 +215,7 @@ exports[`lastReturnedWith works with Set 2`] = ` Expected mock function to have last returned: Set {3, 4} -But it last returned: +But the last call returned: Set {1, 2}" `; @@ -217,10 +233,19 @@ exports[`lastReturnedWith works with argument that does not match 1`] = ` Expected mock function to have last returned: \\"bar\\" -But it last returned: +But the last call returned: \\"foo\\"" `; +exports[`lastReturnedWith works with undefined 1`] = ` +"expect(jest.fn()).not.lastReturnedWith(expected) + +Expected mock function to not have last returned: + undefined +But it last returned exactly: + undefined" +`; + exports[`nthCalledWith includes the custom mock name in the error message 1`] = ` "expect(named-mock).not.nthCalledWith(expected) @@ -355,16 +380,40 @@ Expected mock function first call to have been called with: Did not expect argument 2 but it was called with undefined." `; +exports[`nthReturnedWith a call that throws is not considered to have returned 1`] = ` +"expect(jest.fn()).nthReturnedWith(expected) + +Expected mock function first call to have returned with: + undefined +But the first call threw an error" +`; + +exports[`nthReturnedWith a call that throws undefined is not considered to have returned 1`] = ` +"expect(jest.fn()).nthReturnedWith(expected) + +Expected mock function first call to have returned with: + undefined +But the first call threw an error" +`; + exports[`nthReturnedWith includes the custom mock name in the error message 1`] = ` "expect(named-mock).nthReturnedWith(expected) Expected mock function \\"named-mock\\" first call to have returned with: \\"foo\\" -But it did not return" +But it was not called" `; exports[`nthReturnedWith should reject non integer nth value 1`] = `"nth value 0.1 must be a positive integer greater than 0"`; +exports[`nthReturnedWith should reject nth value greater than number of calls 1`] = ` +"expect(jest.fn()).nthReturnedWith(expected) + +Expected mock function 4th call to have returned with: + \\"foo\\" +But it was only called 3 times" +`; + exports[`nthReturnedWith should reject nth value smaller than 1 1`] = `"nth value 0 must be a positive integer greater than 0"`; exports[`nthReturnedWith should replace 1st, 2nd, 3rd with first, second, third 1`] = ` @@ -398,7 +447,7 @@ exports[`nthReturnedWith works when not called 1`] = ` Expected mock function first call to have returned with: \\"foo\\" -But it did not return" +But it was not called" `; exports[`nthReturnedWith works with Immutable.js objects directly created 1`] = ` @@ -482,6 +531,15 @@ But the first call returned exactly: \\"foo1\\"" `; +exports[`nthReturnedWith works with undefined 1`] = ` +"expect(jest.fn()).not.nthReturnedWith(expected) + +Expected mock function first call to not have returned with: + undefined +But the first call returned exactly: + undefined" +`; + exports[`toBeCalled .not fails with any argument passed 1`] = ` "expect(received)[.not].toBeCalled() @@ -1351,12 +1409,28 @@ Expected mock function first call to have been called with: Did not expect argument 2 but it was called with undefined." `; +exports[`toHaveLastReturnedWith a call that throws is not considered to have returned 1`] = ` +"expect(jest.fn()).toHaveLastReturnedWith(expected) + +Expected mock function to have last returned: + undefined +But the last call threw an error" +`; + +exports[`toHaveLastReturnedWith a call that throws undefined is not considered to have returned 1`] = ` +"expect(jest.fn()).toHaveLastReturnedWith(expected) + +Expected mock function to have last returned: + undefined +But the last call threw an error" +`; + exports[`toHaveLastReturnedWith includes the custom mock name in the error message 1`] = ` "expect(named-mock).toHaveLastReturnedWith(expected) Expected mock function \\"named-mock\\" to have last returned: \\"foo\\" -But it did not return" +But it was not called" `; exports[`toHaveLastReturnedWith works only on spies or jest.fn 1`] = ` @@ -1372,7 +1446,7 @@ exports[`toHaveLastReturnedWith works when not called 1`] = ` Expected mock function to have last returned: \\"foo\\" -But it did not return" +But it was not called" `; exports[`toHaveLastReturnedWith works with Immutable.js objects directly created 1`] = ` @@ -1407,7 +1481,7 @@ exports[`toHaveLastReturnedWith works with Map 2`] = ` Expected mock function to have last returned: Map {\\"a\\" => \\"b\\", \\"b\\" => \\"a\\"} -But it last returned: +But the last call returned: Map {1 => 2, 2 => 1}" `; @@ -1425,7 +1499,7 @@ exports[`toHaveLastReturnedWith works with Set 2`] = ` Expected mock function to have last returned: Set {3, 4} -But it last returned: +But the last call returned: Set {1, 2}" `; @@ -1443,20 +1517,53 @@ exports[`toHaveLastReturnedWith works with argument that does not match 1`] = ` Expected mock function to have last returned: \\"bar\\" -But it last returned: +But the last call returned: \\"foo\\"" `; +exports[`toHaveLastReturnedWith works with undefined 1`] = ` +"expect(jest.fn()).not.toHaveLastReturnedWith(expected) + +Expected mock function to not have last returned: + undefined +But it last returned exactly: + undefined" +`; + +exports[`toHaveNthReturnedWith a call that throws is not considered to have returned 1`] = ` +"expect(jest.fn()).toHaveNthReturnedWith(expected) + +Expected mock function first call to have returned with: + undefined +But the first call threw an error" +`; + +exports[`toHaveNthReturnedWith a call that throws undefined is not considered to have returned 1`] = ` +"expect(jest.fn()).toHaveNthReturnedWith(expected) + +Expected mock function first call to have returned with: + undefined +But the first call threw an error" +`; + exports[`toHaveNthReturnedWith includes the custom mock name in the error message 1`] = ` "expect(named-mock).toHaveNthReturnedWith(expected) Expected mock function \\"named-mock\\" first call to have returned with: \\"foo\\" -But it did not return" +But it was not called" `; exports[`toHaveNthReturnedWith should reject non integer nth value 1`] = `"nth value 0.1 must be a positive integer greater than 0"`; +exports[`toHaveNthReturnedWith should reject nth value greater than number of calls 1`] = ` +"expect(jest.fn()).toHaveNthReturnedWith(expected) + +Expected mock function 4th call to have returned with: + \\"foo\\" +But it was only called 3 times" +`; + exports[`toHaveNthReturnedWith should reject nth value smaller than 1 1`] = `"nth value 0 must be a positive integer greater than 0"`; exports[`toHaveNthReturnedWith should replace 1st, 2nd, 3rd with first, second, third 1`] = ` @@ -1490,7 +1597,7 @@ exports[`toHaveNthReturnedWith works when not called 1`] = ` Expected mock function first call to have returned with: \\"foo\\" -But it did not return" +But it was not called" `; exports[`toHaveNthReturnedWith works with Immutable.js objects directly created 1`] = ` @@ -1574,6 +1681,15 @@ But the first call returned exactly: \\"foo1\\"" `; +exports[`toHaveNthReturnedWith works with undefined 1`] = ` +"expect(jest.fn()).not.toHaveNthReturnedWith(expected) + +Expected mock function first call to not have returned with: + undefined +But the first call returned exactly: + undefined" +`; + exports[`toHaveReturned .not fails with any argument passed 1`] = ` "expect(received)[.not].toHaveReturned() @@ -1582,6 +1698,18 @@ Got: number: 555" `; +exports[`toHaveReturned .not passes when a call throws undefined 1`] = ` +"expect(jest.fn()).toHaveReturned() + +Expected mock function to have returned." +`; + +exports[`toHaveReturned .not passes when all calls throw 1`] = ` +"expect(jest.fn()).toHaveReturned() + +Expected mock function to have returned." +`; + exports[`toHaveReturned .not passes when not returned 1`] = ` "expect(jest.fn()).toHaveReturned() @@ -1603,6 +1731,15 @@ Expected mock function \\"named-mock\\" not to have returned, but it returned: 42" `; +exports[`toHaveReturned passes when at least one call does not throw 1`] = ` +"expect(jest.fn()).not.toHaveReturned() + +Expected mock function not to have returned, but it returned: + 42 + + 42" +`; + exports[`toHaveReturned passes when returned 1`] = ` "expect(jest.fn()).not.toHaveReturned() @@ -1610,6 +1747,13 @@ Expected mock function not to have returned, but it returned: 42" `; +exports[`toHaveReturned passes when undefined is returned 1`] = ` +"expect(jest.fn()).not.toHaveReturned() + +Expected mock function not to have returned, but it returned: + undefined" +`; + exports[`toHaveReturned works only on spies or jest.fn 1`] = ` "expect(jest.fn())[.not].toHaveReturned() @@ -1678,6 +1822,24 @@ exports[`toHaveReturnedTimes .not passes if function returned more than expected Expected mock function to have returned two times, but it returned three times." `; +exports[`toHaveReturnedTimes calls that return undefined are counted as returns 1`] = ` +"expect(jest.fn()).not.toHaveReturnedTimes(2) + +Expected mock function not to have returned two times, but it returned exactly two times." +`; + +exports[`toHaveReturnedTimes calls that throw are not counted 1`] = ` +"expect(jest.fn()).not.toHaveReturnedTimes(2) + +Expected mock function not to have returned two times, but it returned exactly two times." +`; + +exports[`toHaveReturnedTimes calls that throw undefined are not counted 1`] = ` +"expect(jest.fn()).not.toHaveReturnedTimes(2) + +Expected mock function not to have returned two times, but it returned exactly two times." +`; + exports[`toHaveReturnedTimes includes the custom mock name in the error message 1`] = ` "expect(named-mock).toHaveReturnedTimes(1) @@ -1746,6 +1908,22 @@ Received: function: [Function fn]" `; +exports[`toHaveReturnedWith a call that throws is not considered to have returned 1`] = ` +"expect(jest.fn()).toHaveReturnedWith(expected) + +Expected mock function to have returned: + undefined +But it did not return." +`; + +exports[`toHaveReturnedWith a call that throws undefined is not considered to have returned 1`] = ` +"expect(jest.fn()).toHaveReturnedWith(expected) + +Expected mock function to have returned: + undefined +But it did not return." +`; + exports[`toHaveReturnedWith includes the custom mock name in the error message 1`] = ` "expect(named-mock).toHaveReturnedWith(expected) @@ -1861,6 +2039,15 @@ But it returned: ...and 1 more" `; +exports[`toHaveReturnedWith works with undefined 1`] = ` +"expect(jest.fn()).not.toHaveReturnedWith(expected) + +Expected mock function not to have returned: + undefined +But it returned exactly: + undefined" +`; + exports[`toReturn .not fails with any argument passed 1`] = ` "expect(received)[.not].toReturn() @@ -1869,6 +2056,18 @@ Got: number: 555" `; +exports[`toReturn .not passes when a call throws undefined 1`] = ` +"expect(jest.fn()).toReturn() + +Expected mock function to have returned." +`; + +exports[`toReturn .not passes when all calls throw 1`] = ` +"expect(jest.fn()).toReturn() + +Expected mock function to have returned." +`; + exports[`toReturn .not passes when not returned 1`] = ` "expect(jest.fn()).toReturn() @@ -1890,6 +2089,15 @@ Expected mock function \\"named-mock\\" not to have returned, but it returned: 42" `; +exports[`toReturn passes when at least one call does not throw 1`] = ` +"expect(jest.fn()).not.toReturn() + +Expected mock function not to have returned, but it returned: + 42 + + 42" +`; + exports[`toReturn passes when returned 1`] = ` "expect(jest.fn()).not.toReturn() @@ -1897,6 +2105,13 @@ Expected mock function not to have returned, but it returned: 42" `; +exports[`toReturn passes when undefined is returned 1`] = ` +"expect(jest.fn()).not.toReturn() + +Expected mock function not to have returned, but it returned: + undefined" +`; + exports[`toReturn works only on spies or jest.fn 1`] = ` "expect(jest.fn())[.not].toReturn() @@ -1965,6 +2180,24 @@ exports[`toReturnTimes .not passes if function returned more than expected times Expected mock function to have returned two times, but it returned three times." `; +exports[`toReturnTimes calls that return undefined are counted as returns 1`] = ` +"expect(jest.fn()).not.toReturnTimes(2) + +Expected mock function not to have returned two times, but it returned exactly two times." +`; + +exports[`toReturnTimes calls that throw are not counted 1`] = ` +"expect(jest.fn()).not.toReturnTimes(2) + +Expected mock function not to have returned two times, but it returned exactly two times." +`; + +exports[`toReturnTimes calls that throw undefined are not counted 1`] = ` +"expect(jest.fn()).not.toReturnTimes(2) + +Expected mock function not to have returned two times, but it returned exactly two times." +`; + exports[`toReturnTimes includes the custom mock name in the error message 1`] = ` "expect(named-mock).toReturnTimes(1) @@ -2033,6 +2266,22 @@ Received: function: [Function fn]" `; +exports[`toReturnWith a call that throws is not considered to have returned 1`] = ` +"expect(jest.fn()).toReturnWith(expected) + +Expected mock function to have returned: + undefined +But it did not return." +`; + +exports[`toReturnWith a call that throws undefined is not considered to have returned 1`] = ` +"expect(jest.fn()).toReturnWith(expected) + +Expected mock function to have returned: + undefined +But it did not return." +`; + exports[`toReturnWith includes the custom mock name in the error message 1`] = ` "expect(named-mock).toReturnWith(expected) @@ -2147,3 +2396,12 @@ But it returned: ...and 1 more" `; + +exports[`toReturnWith works with undefined 1`] = ` +"expect(jest.fn()).not.toReturnWith(expected) + +Expected mock function not to have returned: + undefined +But it returned exactly: + undefined" +`; diff --git a/packages/expect/src/__tests__/spy_matchers.test.js b/packages/expect/src/__tests__/spy_matchers.test.js index 4df07681b37e..3ae90bfe7b11 100644 --- a/packages/expect/src/__tests__/spy_matchers.test.js +++ b/packages/expect/src/__tests__/spy_matchers.test.js @@ -388,6 +388,40 @@ const jestExpect = require('../'); ).toThrowErrorMatchingSnapshot(); }); + test(`passes when undefined is returned`, () => { + const fn = jest.fn(() => undefined); + fn(); + jestExpect(fn)[returned](); + expect(() => + jestExpect(fn).not[returned](), + ).toThrowErrorMatchingSnapshot(); + }); + + test(`passes when at least one call does not throw`, () => { + const fn = jest.fn(causeError => { + if (causeError) { + throw new Error('Error!'); + } + + return 42; + }); + + fn(false); + + try { + fn(true); + } catch (error) { + // ignore error + } + + fn(false); + + jestExpect(fn)[returned](); + expect(() => + jestExpect(fn).not[returned](), + ).toThrowErrorMatchingSnapshot(); + }); + test(`.not passes when not returned`, () => { const fn = jest.fn(); @@ -395,6 +429,43 @@ const jestExpect = require('../'); expect(() => jestExpect(fn)[returned]()).toThrowErrorMatchingSnapshot(); }); + test(`.not passes when all calls throw`, () => { + const fn = jest.fn(() => { + throw new Error('Error!'); + }); + + try { + fn(); + } catch (error) { + // ignore error + } + + try { + fn(); + } catch (error) { + // ignore error + } + + jestExpect(fn).not[returned](); + expect(() => jestExpect(fn)[returned]()).toThrowErrorMatchingSnapshot(); + }); + + test(`.not passes when a call throws undefined`, () => { + const fn = jest.fn(() => { + // eslint-disable-next-line no-throw-literal + throw undefined; + }); + + try { + fn(); + } catch (error) { + // ignore error + } + + jestExpect(fn).not[returned](); + expect(() => jestExpect(fn)[returned]()).toThrowErrorMatchingSnapshot(); + }); + test(`fails with any argument passed`, () => { const fn = jest.fn(); @@ -468,6 +539,18 @@ const jestExpect = require('../'); ).toThrowErrorMatchingSnapshot(); }); + test('calls that return undefined are counted as returns', () => { + const fn = jest.fn(() => undefined); + fn(); + fn(); + + jestExpect(fn)[returnedTimes](2); + + expect(() => + jestExpect(fn).not[returnedTimes](2), + ).toThrowErrorMatchingSnapshot(); + }); + test('.not passes if function returned more than expected times', () => { const fn = jest.fn(() => 42); fn(); @@ -494,6 +577,59 @@ const jestExpect = require('../'); ).toThrowErrorMatchingSnapshot(); }); + test('calls that throw are not counted', () => { + const fn = jest.fn(causeError => { + if (causeError) { + throw new Error('Error!'); + } + + return 42; + }); + + fn(false); + + try { + fn(true); + } catch (error) { + // ignore error + } + + fn(false); + + jestExpect(fn)[returnedTimes](2); + + expect(() => + jestExpect(fn).not[returnedTimes](2), + ).toThrowErrorMatchingSnapshot(); + }); + + test('calls that throw undefined are not counted', () => { + const fn = jest.fn(causeError => { + if (causeError) { + // eslint-disable-next-line no-throw-literal + throw undefined; + } + + return 42; + }); + + fn(false); + + try { + fn(true); + } catch (error) { + // ignore error + } + + fn(false); + + jestExpect(fn)[returnedTimes](2); + + expect(() => + jestExpect(fn).not[returnedTimes](2), + ).toThrowErrorMatchingSnapshot(); + }); + test('includes the custom mock name in the error message', () => { const fn = jest.fn(() => 42).mockName('named-mock'); fn(); @@ -573,6 +709,17 @@ const jestExpect = require('../'); ).toThrowErrorMatchingSnapshot(); }); + test(`works with undefined`, () => { + const fn = jest.fn(() => undefined); + fn(); + + caller(jestExpect(fn)[returnedWith], undefined); + + expect(() => + caller(jestExpect(fn).not[returnedWith], undefined), + ).toThrowErrorMatchingSnapshot(); + }); + test(`works with Map`, () => { const m1 = new Map([[1, 2], [2, 1]]); const m2 = new Map([[1, 2], [2, 1]]); @@ -635,6 +782,49 @@ const jestExpect = require('../'); ).toThrowErrorMatchingSnapshot(); }); + test(`a call that throws is not considered to have returned`, () => { + const fn = jest.fn(() => { + throw new Error('Error!'); + }); + + try { + fn(); + } catch (error) { + // ignore error + } + + // It doesn't matter what return value is tested if the call threw + caller(jestExpect(fn).not[returnedWith], 'foo'); + caller(jestExpect(fn).not[returnedWith], null); + caller(jestExpect(fn).not[returnedWith], undefined); + + expect(() => + caller(jestExpect(fn)[returnedWith], undefined), + ).toThrowErrorMatchingSnapshot(); + }); + + test(`a call that throws undefined is not considered to have returned`, () => { + const fn = jest.fn(() => { + // eslint-disable-next-line no-throw-literal + throw undefined; + }); + + try { + fn(); + } catch (error) { + // ignore error + } + + // It doesn't matter what return value is tested if the call threw + caller(jestExpect(fn).not[returnedWith], 'foo'); + caller(jestExpect(fn).not[returnedWith], null); + caller(jestExpect(fn).not[returnedWith], undefined); + + expect(() => + caller(jestExpect(fn)[returnedWith], undefined), + ).toThrowErrorMatchingSnapshot(); + }); + const basicReturnedWith = ['toHaveReturnedWith', 'toReturnWith']; if (basicReturnedWith.indexOf(returnedWith) >= 0) { test(`works with more calls than the limit`, () => { @@ -714,6 +904,17 @@ const jestExpect = require('../'); }).toThrowErrorMatchingSnapshot(); }); + test('should reject nth value greater than number of calls', async () => { + const fn = jest.fn(() => 'foo'); + fn(); + fn(); + fn(); + + expect(() => { + jestExpect(fn)[returnedWith](4, 'foo'); + }).toThrowErrorMatchingSnapshot(); + }); + test('should reject non integer nth value', async () => { const fn = jest.fn(() => 'foo'); fn('foo'); diff --git a/packages/expect/src/spy_matchers.js b/packages/expect/src/spy_matchers.js index 82d567f15937..e7404c58af7b 100644 --- a/packages/expect/src/spy_matchers.js +++ b/packages/expect/src/spy_matchers.js @@ -69,18 +69,23 @@ const createToReturnMatcher = matcherName => (received, expected) => { ? 'mock function' : `mock function "${receivedName}"`; - const returnValues = received.mock.returnValues; + // List of return values that correspond only to calls that did not throw + // an error + const returnValues = received.mock.results + .filter(result => !result.isThrow) + .map(result => result.value); + const count = returnValues.length; const pass = count > 0; const message = pass ? () => - matcherHint('.not' + matcherName, received.getMockName(), '') + + matcherHint('.not' + matcherName, receivedName, '') + '\n\n' + `Expected ${identifier} not to have returned, but it returned:\n` + ` ${getPrintedReturnValues(returnValues, RETURN_PRINT_LIMIT)}` : () => - matcherHint(matcherName, received.getMockName(), '') + + matcherHint(matcherName, receivedName, '') + '\n\n' + `Expected ${identifier} to have returned.`; @@ -135,22 +140,22 @@ const createToReturnTimesMatcher = (matcherName: string) => ( ? 'mock function' : `mock function "${receivedName}"`; - const count = received.mock.returnValues.length; + // List of return results that correspond only to calls that did not throw + // an error + const returnResults = received.mock.results.filter(result => !result.isThrow); + + const count = returnResults.length; const pass = count === expected; const message = pass ? () => - matcherHint( - '.not' + matcherName, - received.getMockName(), - String(expected), - ) + + matcherHint('.not' + matcherName, receivedName, String(expected)) + `\n\n` + `Expected ${identifier} not to have returned ` + `${EXPECTED_COLOR(pluralize('time', expected))}, but it` + ` returned exactly ${RECEIVED_COLOR(pluralize('time', count))}.` : () => - matcherHint(matcherName, received.getMockName(), String(expected)) + + matcherHint(matcherName, receivedName, String(expected)) + '\n\n' + `Expected ${identifier} to have returned ` + `${EXPECTED_COLOR(pluralize('time', expected))},` + @@ -209,7 +214,12 @@ const createToReturnWithMatcher = matcherName => ( ? 'mock function' : `mock function "${receivedName}"`; - const returnValues = received.mock.returnValues; + // List of return values that correspond only to calls that did not throw + // an error + const returnValues = received.mock.results + .filter(result => !result.isThrow) + .map(result => result.value); + const [match] = partition(returnValues, value => equals(expected, value, [iterableEquality]), ); @@ -217,14 +227,14 @@ const createToReturnWithMatcher = matcherName => ( const message = pass ? () => - matcherHint('.not' + matcherName, received.getMockName()) + + matcherHint('.not' + matcherName, receivedName) + '\n\n' + `Expected ${identifier} not to have returned:\n` + ` ${printExpected(expected)}\n` + `But it returned exactly:\n` + ` ${printReceived(expected)}` : () => - matcherHint(matcherName, received.getMockName()) + + matcherHint(matcherName, receivedName) + '\n\n' + `Expected ${identifier} to have returned:\n` + formatMismatchedReturnValues( @@ -281,26 +291,33 @@ const createLastReturnedMatcher = matcherName => ( ? 'mock function' : `mock function "${receivedName}"`; - const returnValues = received.mock.returnValues; - const lastReturnValue = returnValues[returnValues.length - 1]; - const pass = equals(lastReturnValue, expected, [iterableEquality]); + const results = received.mock.results; + const lastResult = results[results.length - 1]; + const pass = + !!lastResult && + !lastResult.isThrow && + equals(lastResult.value, expected, [iterableEquality]); const message = pass ? () => - matcherHint('.not' + matcherName, received.getMockName()) + + matcherHint('.not' + matcherName, receivedName) + '\n\n' + `Expected ${identifier} to not have last returned:\n` + ` ${printExpected(expected)}\n` + `But it last returned exactly:\n` + - ` ${printReceived(lastReturnValue)}` + ` ${printReceived(lastResult.value)}` : () => - matcherHint(matcherName, received.getMockName()) + + matcherHint(matcherName, receivedName) + '\n\n' + `Expected ${identifier} to have last returned:\n` + ` ${printExpected(expected)}\n` + - (returnValues.length > 0 - ? `But it last returned:\n ${printReceived(lastReturnValue)}` - : `But it did ${RECEIVED_COLOR('not return')}`); + (!lastResult + ? `But it was ${RECEIVED_COLOR('not called')}` + : lastResult.isThrow + ? `But the last call ${RECEIVED_COLOR('threw an error')}` + : `But the last call returned:\n ${printReceived( + lastResult.value, + )}`); return {message, pass}; }; @@ -375,28 +392,35 @@ const createNthReturnedWithMatcher = (matcherName: string) => ( ? 'mock function' : `mock function "${receivedName}"`; - const returnValues = received.mock.returnValues; - const nthValue = returnValues[nth - 1]; - const pass = equals(nthValue, expected, [iterableEquality]); + const results = received.mock.results; + const nthResult = results[nth - 1]; + const pass = + !!nthResult && + !nthResult.isThrow && + equals(nthResult.value, expected, [iterableEquality]); const nthString = nthToString(nth); const message = pass ? () => - matcherHint('.not' + matcherName, received.getMockName()) + + matcherHint('.not' + matcherName, receivedName) + '\n\n' + `Expected ${identifier} ${nthString} call to not have returned with:\n` + ` ${printExpected(expected)}\n` + `But the ${nthString} call returned exactly:\n` + - ` ${printReceived(nthValue)}` + ` ${printReceived(nthResult.value)}` : () => - matcherHint(matcherName, received.getMockName()) + + matcherHint(matcherName, receivedName) + '\n\n' + `Expected ${identifier} ${nthString} call to have returned with:\n` + ` ${printExpected(expected)}\n` + - (returnValues.length > 0 - ? `But the ${nthString} call returned with:\n ${printReceived( - nthValue, - )}` - : `But it did ${RECEIVED_COLOR('not return')}`); + (results.length === 0 + ? `But it was ${RECEIVED_COLOR('not called')}` + : nth > results.length + ? `But it was only called ${printReceived(results.length)} times` + : nthResult.isThrow + ? `But the ${nthString} call ${RECEIVED_COLOR('threw an error')}` + : `But the ${nthString} call returned with:\n ${printReceived( + nthResult.value, + )}`); return {message, pass}; }; diff --git a/packages/jest-mock/src/__tests__/jest_mock.test.js b/packages/jest-mock/src/__tests__/jest_mock.test.js index 6c2437f4b43a..7964bd22a235 100644 --- a/packages/jest-mock/src/__tests__/jest_mock.test.js +++ b/packages/jest-mock/src/__tests__/jest_mock.test.js @@ -442,12 +442,21 @@ describe('moduleMocker', () => { it('tracks return values', () => { const fn = moduleMocker.fn(x => x * 2); - expect(fn.mock.returnValues).toEqual([]); + expect(fn.mock.results).toEqual([]); fn(1); fn(2); - expect(fn.mock.returnValues).toEqual([2, 4]); + expect(fn.mock.results).toEqual([ + { + isThrow: false, + value: 2, + }, + { + isThrow: false, + value: 4, + }, + ]); }); it('tracks mocked return values', () => { @@ -457,22 +466,40 @@ describe('moduleMocker', () => { fn(1); fn(2); - expect(fn.mock.returnValues).toEqual(['MOCKED!', 4]); + expect(fn.mock.results).toEqual([ + { + isThrow: false, + value: 'MOCKED!', + }, + { + isThrow: false, + value: 4, + }, + ]); }); it('supports resetting return values', () => { const fn = moduleMocker.fn(x => x * 2); - expect(fn.mock.returnValues).toEqual([]); + expect(fn.mock.results).toEqual([]); fn(1); fn(2); - expect(fn.mock.returnValues).toEqual([2, 4]); + expect(fn.mock.results).toEqual([ + { + isThrow: false, + value: 2, + }, + { + isThrow: false, + value: 4, + }, + ]); fn.mockReset(); - expect(fn.mock.returnValues).toEqual([]); + expect(fn.mock.results).toEqual([]); }); }); @@ -502,10 +529,44 @@ describe('moduleMocker', () => { // 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]); + // Results are tracked + expect(fn.mock.results).toEqual([ + { + isThrow: false, + value: 8, + }, + { + isThrow: true, + value: error, + }, + { + isThrow: false, + value: 18, + }, + ]); + }); + + it(`a call that throws undefined is tracked properly`, () => { + const fn = moduleMocker.fn(() => { + // eslint-disable-next-line no-throw-literal + throw undefined; + }); + + try { + fn(2, 4); + } catch (error) { + // ignore error + } + + // All call args tracked + expect(fn.mock.calls).toEqual([[2, 4]]); + // Results are tracked + expect(fn.mock.results).toEqual([ + { + isThrow: true, + value: undefined, + }, + ]); }); describe('invocationCallOrder', () => { diff --git a/packages/jest-mock/src/index.js b/packages/jest-mock/src/index.js index 69a02a4860cf..db2e2134802f 100644 --- a/packages/jest-mock/src/index.js +++ b/packages/jest-mock/src/index.js @@ -21,11 +21,28 @@ export type MockFunctionMetadata = { length?: number, }; +/** + * Represents the result of a single call to a mock function. + */ +type MockFunctionResult = { + /** + * True if the function threw. + * False if the function returned. + */ + isThrow: boolean, + /** + * The value that was either thrown or returned by the function. + */ + value: any, +}; + type MockFunctionState = { instances: Array, calls: Array>, - returnValues: Array, - thrownErrors: Array, + /** + * List of results of calls to the mock function. + */ + results: Array, invocationCallOrder: Array, }; @@ -291,8 +308,7 @@ class ModuleMockerClass { calls: [], instances: [], invocationCallOrder: [], - returnValues: [], - thrownErrors: [], + results: [], }; } @@ -333,6 +349,10 @@ class ModuleMockerClass { let finalReturnValue; // Will be set to the error that is thrown by the mock (if it throws) let thrownError; + // Will be set to true if the mock throws an error. The presence of a + // value in `thrownError` is not a 100% reliable indicator because a + // function could throw a value of undefined. + let callDidThrowError = false; try { // The bulk of the implementation is wrapped in an immediately @@ -398,14 +418,14 @@ class ModuleMockerClass { } catch (error) { // Store the thrown error so we can record it, then re-throw it. thrownError = error; + callDidThrowError = true; 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 result of the function + mockState.results.push({ + isThrow: callDidThrowError, + value: callDidThrowError ? thrownError : finalReturnValue, + }); } return finalReturnValue;