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

Enhance function mocks to expose a list of returned values. #5752

Merged
merged 3 commits into from
Mar 8, 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
([#5670](https://github.com/facebook/jest/pull/5670))
* `[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))
* `[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
15 changes: 14 additions & 1 deletion docs/MockFunctionAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,26 @@ Each call is represented by 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
`f('arg1', 'arg2')`, and then with the arguments `f('arg3', 'arg4')`, would have
a `mock.calls` array that looks like this:

```js
[['arg1', 'arg2'], ['arg3', 'arg4']];
```

### `mockFn.mock.returnValues`

An array containing values that have been returned 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:

```js
['result1', 'result2'];
```

### `mockFn.mock.instances`

An array that contains all the object instances that have been instantiated from
Expand Down
13 changes: 10 additions & 3 deletions docs/MockFunctions.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@ expect(mockCallback.mock.calls[0][0]).toBe(0);

// The first argument of the second call to the function was 1
expect(mockCallback.mock.calls[1][0]).toBe(1);

// The return value of the first call to the function was 42
expect(mockCallback.mock.returnValues[0]).toBe(42);
```

## `.mock` property

All mock functions have this special `.mock` property, which is where data about
how the function has been called is kept. The `.mock` property also tracks the
value of `this` for each call, so it is possible to inspect this as well:
how the function has been called and what the function returned is kept. The
`.mock` property also tracks the value of `this` for each call, so it is
possible to inspect this as well:

```javascript
const myMock = jest.fn();
Expand All @@ -62,7 +66,7 @@ console.log(myMock.mock.instances);
```

These mock members are very useful in tests to assert how these functions get
called, or instantiated:
called, instantiated, or what they returned:

```javascript
// The function was called exactly once
Expand All @@ -74,6 +78,9 @@ expect(someMockFunction.mock.calls[0][0]).toBe('first arg');
// The second arg of the first call to the function was 'second arg'
expect(someMockFunction.mock.calls[0][1]).toBe('second arg');

// The return value of the first call to the function was 'return value'
expect(someMockFunction.mock.returnValues[0]).toBe('return value');

// This function was instantiated exactly twice
expect(someMockFunction.mock.instances.length).toBe(2);

Expand Down
38 changes: 38 additions & 0 deletions packages/jest-mock/src/__tests__/jest_mock.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,44 @@ describe('moduleMocker', () => {
]);
});

describe('return values', () => {
it('tracks return values', () => {
const fn = moduleMocker.fn(x => x * 2);

expect(fn.mock.returnValues).toEqual([]);

fn(1);
fn(2);

expect(fn.mock.returnValues).toEqual([2, 4]);
});

it('tracks mocked return values', () => {
const fn = moduleMocker.fn(x => x * 2);
fn.mockReturnValueOnce('MOCKED!');

fn(1);
fn(2);

expect(fn.mock.returnValues).toEqual(['MOCKED!', 4]);
});

it('supports resetting return values', () => {
const fn = moduleMocker.fn(x => x * 2);

expect(fn.mock.returnValues).toEqual([]);

fn(1);
fn(2);

expect(fn.mock.returnValues).toEqual([2, 4]);

fn.mockReset();

expect(fn.mock.returnValues).toEqual([]);
});
});

describe('timestamps', () => {
const RealDate = Date;

Expand Down
101 changes: 57 additions & 44 deletions packages/jest-mock/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export type MockFunctionMetadata = {
type MockFunctionState = {
instances: Array<any>,
calls: Array<Array<any>>,
returnValues: Array<any>,
timestamps: Array<number>,
};

Expand Down Expand Up @@ -280,6 +281,7 @@ class ModuleMockerClass {
return {
calls: [],
instances: [],
returnValues: [],
timestamps: [],
};
}
Expand Down Expand Up @@ -316,58 +318,69 @@ class ModuleMockerClass {
mockState.instances.push(this);
mockState.calls.push(Array.prototype.slice.call(arguments));
mockState.timestamps.push(Date.now());
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);
}
// 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) {
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 inside of this immediately executed arrow function was simply indented and wrapped in the arrow function with no other changes. This seemed to be the safest way to capture the return value.

// 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;
}
});

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();
}
// 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;
// Record the return value of the mock function before returning it.
mockState.returnValues.push(finalReturnValue);
return finalReturnValue;
}, metadata.length || 0);

f = this._createMockFunction(metadata, mockConstructor);
Expand Down