Skip to content

Commit

Permalink
[jest-mock] Track thrown errors in MockFunctionState (#5764)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
UselessPickles authored and cpojer committed Mar 11, 2018
1 parent be1aee6 commit 2a6fc70
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 59 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 20 additions & 4 deletions docs/MockFunctionAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
32 changes: 32 additions & 0 deletions packages/jest-mock/src/__tests__/jest_mock.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
128 changes: 74 additions & 54 deletions packages/jest-mock/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type MockFunctionState = {
instances: Array<any>,
calls: Array<Array<any>>,
returnValues: Array<any>,
thrownErrors: Array<any>,
timestamps: Array<number>,
};

Expand Down Expand Up @@ -282,6 +283,7 @@ class ModuleMockerClass {
calls: [],
instances: [],
returnValues: [],
thrownErrors: [],
timestamps: [],
};
}
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 2a6fc70

Please sign in to comment.