Skip to content

Commit

Permalink
Enhance function mocks to expose a list of returned values. (jestjs#5752
Browse files Browse the repository at this point in the history
)

* Enhance function mocks to maintain a list of returned values.

* Update CHANGELOG
  • Loading branch information
UselessPickles authored and thomasjinlo committed Mar 9, 2018
1 parent 08f5353 commit a137549
Show file tree
Hide file tree
Showing 5 changed files with 121 additions and 48 deletions.
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) {
// 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

0 comments on commit a137549

Please sign in to comment.