Skip to content

Commit 2a6fc70

Browse files
UselessPicklescpojer
authored andcommitted
[jest-mock] Track thrown errors in MockFunctionState (#5764)
* Fix changelog from previous PR to link to PR. * jest-mock: Track thrown errors in MockFunctionState. Also fix bug where returnValues array was not updated when an error is thrown. * Update CHANGELOG * Improve mock.returnValues API documentation
1 parent be1aee6 commit 2a6fc70

4 files changed

Lines changed: 129 additions & 59 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
* `[expect]` Add inverse matchers (`expect.not.arrayContaining`, etc.,
1818
[#5517](https://github.com/facebook/jest/pull/5517))
1919
* `[jest-mock]` Add tracking of return values in the `mock` property
20-
([#5738](https://github.com/facebook/jest/issues/5738))
20+
([#5752](https://github.com/facebook/jest/pull/5752))
21+
* `[jest-mock]` Add tracking of thrown errors in the `mock` property
22+
([5764](https://github.com/facebook/jest/pull/5764))
2123
* `[expect]`Add nthCalledWith spy matcher
2224
([#5605](https://github.com/facebook/jest/pull/5605))
2325
* `[jest-cli]` Add `isSerial` property that runners can expose to specify that

docs/MockFunctionAPI.md

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,30 @@ a `mock.calls` array that looks like this:
3939
### `mockFn.mock.returnValues`
4040

4141
An array containing values that have been returned by all calls to this mock
42+
function. For any call to the mock that throws an error, a value of `undefined`
43+
will be stored in `mock.returnValues`.
44+
45+
For example: A mock function `f` that has been called three times, returning
46+
`result1`, throwing an error, and then returning `result2`, would have a
47+
`mock.returnValues` array that looks like this:
48+
49+
```js
50+
['result1', undefined, 'result2'];
51+
```
52+
53+
### `mockFn.mock.thrownErrors`
54+
55+
An array containing errors that have been thrown by all calls to this mock
4256
function.
4357

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:
58+
For example: A mock function `f` that has been called twice, throwing an
59+
`Error`, and then executing successfully without an error, would have the
60+
following `mock.thrownErrors` array:
4761

4862
```js
49-
['result1', 'result2'];
63+
f.mock.thrownErrors.length === 2; // true
64+
f.mock.thrownErrors[0] instanceof Error; // true
65+
f.mock.thrownErrors[1] === undefined; // true
5066
```
5167

5268
### `mockFn.mock.instances`

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,38 @@ describe('moduleMocker', () => {
476476
});
477477
});
478478

479+
it(`tracks thrown errors without interfering with other tracking`, () => {
480+
const error = new Error('ODD!');
481+
const fn = moduleMocker.fn((x, y) => {
482+
// multiply params
483+
const result = x * y;
484+
485+
if (result % 2 === 1) {
486+
// throw error if result is odd
487+
throw error;
488+
} else {
489+
return result;
490+
}
491+
});
492+
493+
expect(fn(2, 4)).toBe(8);
494+
495+
// Mock still throws the error even though it was internally
496+
// caught and recorded
497+
expect(() => {
498+
fn(3, 5);
499+
}).toThrow('ODD!');
500+
501+
expect(fn(6, 3)).toBe(18);
502+
503+
// All call args tracked
504+
expect(fn.mock.calls).toEqual([[2, 4], [3, 5], [6, 3]]);
505+
// tracked return value is undefined when an error is thrown
506+
expect(fn.mock.returnValues).toEqual([8, undefined, 18]);
507+
// tracked thrown error is undefined when an error is NOT thrown
508+
expect(fn.mock.thrownErrors).toEqual([undefined, error, undefined]);
509+
});
510+
479511
describe('timestamps', () => {
480512
const RealDate = Date;
481513

packages/jest-mock/src/index.js

Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ type MockFunctionState = {
2525
instances: Array<any>,
2626
calls: Array<Array<any>>,
2727
returnValues: Array<any>,
28+
thrownErrors: Array<any>,
2829
timestamps: Array<number>,
2930
};
3031

@@ -282,6 +283,7 @@ class ModuleMockerClass {
282283
calls: [],
283284
instances: [],
284285
returnValues: [],
286+
thrownErrors: [],
285287
timestamps: [],
286288
};
287289
}
@@ -319,67 +321,85 @@ class ModuleMockerClass {
319321
mockState.calls.push(Array.prototype.slice.call(arguments));
320322
mockState.timestamps.push(Date.now());
321323

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-
});
339-
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-
}
346-
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-
}
324+
// Will be set to the return value of the mock if an error is not thrown
325+
let finalReturnValue;
326+
// Will be set to the error that is thrown by the mock (if it throws)
327+
let thrownError;
328+
329+
try {
330+
// The bulk of the implementation is wrapped in an immediately
331+
// executed arrow function so the return value of the mock function
332+
// can be easily captured and recorded, despite the many separate
333+
// return points within the logic.
334+
finalReturnValue = (() => {
335+
if (this instanceof f) {
336+
// This is probably being called as a constructor
337+
prototypeSlots.forEach(slot => {
338+
// Copy prototype methods to the instance to make
339+
// it easier to interact with mock instance call and
340+
// return values
341+
if (prototype[slot].type === 'function') {
342+
const protoImpl = this[slot];
343+
this[slot] = mocker.generateFromMetadata(prototype[slot]);
344+
this[slot]._protoImpl = protoImpl;
345+
}
346+
});
347+
348+
// Run the mock constructor implementation
349+
const mockImpl = mockConfig.specificMockImpls.length
350+
? mockConfig.specificMockImpls.shift()
351+
: mockConfig.mockImpl;
352+
return mockImpl && mockImpl.apply(this, arguments);
353+
}
355354

356-
if (mockConfig.isReturnValueLastSet) {
357-
return mockConfig.defaultReturnValue;
358-
}
355+
const returnValue = mockConfig.defaultReturnValue;
356+
// If return value is last set, either specific or default, i.e.
357+
// mockReturnValueOnce()/mockReturnValue() is called and no
358+
// mockImplementationOnce()/mockImplementation() is called after
359+
// that.
360+
// use the set return value.
361+
if (mockConfig.specificReturnValues.length) {
362+
return mockConfig.specificReturnValues.shift();
363+
}
359364

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;
365+
if (mockConfig.isReturnValueLastSet) {
366+
return mockConfig.defaultReturnValue;
367367
}
368-
if (specificMockImpl) {
369-
return specificMockImpl.apply(this, arguments);
368+
369+
// If mockImplementationOnce()/mockImplementation() is last set,
370+
// or specific return values are used up, use the mock
371+
// implementation.
372+
let specificMockImpl;
373+
if (returnValue === undefined) {
374+
specificMockImpl = mockConfig.specificMockImpls.shift();
375+
if (specificMockImpl === undefined) {
376+
specificMockImpl = mockConfig.mockImpl;
377+
}
378+
if (specificMockImpl) {
379+
return specificMockImpl.apply(this, arguments);
380+
}
370381
}
371-
}
372382

373-
// Otherwise use prototype implementation
374-
if (returnValue === undefined && f._protoImpl) {
375-
return f._protoImpl.apply(this, arguments);
376-
}
383+
// Otherwise use prototype implementation
384+
if (returnValue === undefined && f._protoImpl) {
385+
return f._protoImpl.apply(this, arguments);
386+
}
377387

378-
return returnValue;
379-
})();
388+
return returnValue;
389+
})();
390+
} catch (error) {
391+
// Store the thrown error so we can record it, then re-throw it.
392+
thrownError = error;
393+
throw error;
394+
} finally {
395+
// Record the return value of the mock function.
396+
// If the mock threw an error, then the value will be undefined.
397+
mockState.returnValues.push(finalReturnValue);
398+
// Record the error thrown by the mock function.
399+
// If no error was thrown, then the value will be udnefiend.
400+
mockState.thrownErrors.push(thrownError);
401+
}
380402

381-
// Record the return value of the mock function before returning it.
382-
mockState.returnValues.push(finalReturnValue);
383403
return finalReturnValue;
384404
}, metadata.length || 0);
385405

0 commit comments

Comments
 (0)