Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[jest-mock] Track thrown errors in MockFunctionState #5764

Merged
merged 4 commits into from
Mar 11, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything in the try {} block was simply indented with no other changes. The diff makes it look much worse than it is :)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Append a ?w=1 to the diff URL and font bother :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the tip. Would be nicer if this option was exposed in the UI.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can recommend refined github: https://github.com/sindresorhus/refined-github

I have this button:
image

// 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