Skip to content

Commit

Permalink
Add contexts to mocks (#12601)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthias-ccri authored Mar 29, 2022
1 parent daf583c commit f496a93
Show file tree
Hide file tree
Showing 14 changed files with 87 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
- `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442))
- `[jest-mock]` [**BREAKING**] Improve the usage of `jest.fn` generic type argument ([#12489](https://github.com/facebook/jest/pull/12489))
- `[jest-mock]` Add support for auto-mocking async generator functions ([#11080](https://github.com/facebook/jest/pull/11080))
- `[jest-mock]` Add `contexts` member to mock functions ([#12601](https://github.com/facebook/jest/pull/12601))
- `[jest-resolve]` [**BREAKING**] Add support for `package.json` `exports` ([#11961](https://github.com/facebook/jest/pull/11961), [#12373](https://github.com/facebook/jest/pull/12373))
- `[jest-resolve, jest-runtime]` Add support for `data:` URI import and mock ([#12392](https://github.com/facebook/jest/pull/12392))
- `[jest-resolve, jest-runtime]` Add support for async resolver ([#11540](https://github.com/facebook/jest/pull/11540))
Expand Down
2 changes: 1 addition & 1 deletion docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Clearing the cache will reduce performance.

### `--clearMocks`

Automatically clear mock calls, instances and results before every test. Equivalent to calling [`jest.clearAllMocks()`](JestObjectAPI.md#jestclearallmocks) before each test. This does not remove any mock implementation that may have been provided.
Automatically clear mock calls, instances, contexts and results before every test. Equivalent to calling [`jest.clearAllMocks()`](JestObjectAPI.md#jestclearallmocks) before each test. This does not remove any mock implementation that may have been provided.

### `--collectCoverageFrom=<glob>`

Expand Down
2 changes: 1 addition & 1 deletion docs/Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ Jest attempts to scan your dependency tree once (up-front) and cache it in order

Default: `false`

Automatically clear mock calls, instances and results before every test. Equivalent to calling [`jest.clearAllMocks()`](JestObjectAPI.md#jestclearallmocks) before each test. This does not remove any mock implementation that may have been provided.
Automatically clear mock calls, instances, contexts and results before every test. Equivalent to calling [`jest.clearAllMocks()`](JestObjectAPI.md#jestclearallmocks) before each test. This does not remove any mock implementation that may have been provided.

### `collectCoverage` \[boolean]

Expand Down
2 changes: 1 addition & 1 deletion docs/JestObjectAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ test('plays audio', () => {

### `jest.clearAllMocks()`

Clears the `mock.calls`, `mock.instances` and `mock.results` properties of all mocks. Equivalent to calling [`.mockClear()`](MockFunctionAPI.md#mockfnmockclear) on every mocked function.
Clears the `mock.calls`, `mock.instances`, `mock.contexts` and `mock.results` properties of all mocks. Equivalent to calling [`.mockClear()`](MockFunctionAPI.md#mockfnmockclear) on every mocked function.

Returns the `jest` object for chaining.

Expand Down
25 changes: 23 additions & 2 deletions docs/MockFunctionAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,27 @@ mockFn.mock.instances[0] === a; // true
mockFn.mock.instances[1] === b; // true
```

### `mockFn.mock.contexts`

An array that contains the contexts for all calls of the mock function.

A context is the `this` value that a function receives when called. The context can be set using `Function.prototype.bind`, `Function.prototype.call` or `Function.prototype.apply`.

For example:

```js
const mockFn = jest.fn();

const boundMockFn = mockFn.bind(thisContext0);
boundMockFn('a', 'b');
mockFn.call(thisContext1, 'a', 'b');
mockFn.apply(thisContext2, ['a', 'b']);

mockFn.mock.contexts[0] === thisContext0; // true
mockFn.mock.contexts[1] === thisContext1; // true
mockFn.mock.contexts[2] === thisContext2; // true
```

### `mockFn.mock.lastCall`

An array containing the call arguments of the last call that was made to this mock function. If the function was not called, it will return `undefined`.
Expand All @@ -104,9 +125,9 @@ For example: A mock function `f` that has been called twice, with the arguments

### `mockFn.mockClear()`

Clears all information stored in the [`mockFn.mock.calls`](#mockfnmockcalls), [`mockFn.mock.instances`](#mockfnmockinstances) and [`mockFn.mock.results`](#mockfnmockresults) arrays. Often this is useful when you want to clean up a mocks usage data between two assertions.
Clears all information stored in the [`mockFn.mock.calls`](#mockfnmockcalls), [`mockFn.mock.instances`](#mockfnmockinstances), [`mockFn.mock.contexts`](#mockfnmockcontexts) and [`mockFn.mock.results`](#mockfnmockresults) arrays. Often this is useful when you want to clean up a mocks usage data between two assertions.

Beware that `mockFn.mockClear()` will replace `mockFn.mock`, not just these three properties! You should, therefore, avoid assigning `mockFn.mock` to other variables, temporary or not, to make sure you don't access stale data.
Beware that `mockFn.mockClear()` will replace `mockFn.mock`, not just reset the values of its properties! You should, therefore, avoid assigning `mockFn.mock` to other variables, temporary or not, to make sure you don't access stale data.

The [`clearMocks`](configuration#clearmocks-boolean) configuration option is available to clear mocks automatically before each tests.

Expand Down
17 changes: 11 additions & 6 deletions docs/MockFunctions.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,17 @@ expect(mockCallback.mock.results[0].value).toBe(42);
All mock functions have this special `.mock` property, which is where data about 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();
const myMock1 = jest.fn();
const a = new myMock1();
console.log(myMock1.mock.instances);
// > [ <a> ]

const a = new myMock();
const myMock2 = jest.fn();
const b = {};
const bound = myMock.bind(b);
const bound = myMock2.bind(b);
bound();

console.log(myMock.mock.instances);
// > [ <a>, <b> ]
console.log(myMock2.mock.contexts);
// > [ <b> ]
```

These mock members are very useful in tests to assert how these functions get called, instantiated, or what they returned:
Expand All @@ -69,6 +71,9 @@ 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.results[0].value).toBe('return value');

// The function was called with a certain `this` context: the `element` object.
expect(someMockFunction.mock.contexts[0]).toBe(element);

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

Expand Down
2 changes: 1 addition & 1 deletion packages/jest-cli/src/cli/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ export const options = {
},
clearMocks: {
description:
'Automatically clear mock calls, instances and results before every test. ' +
'Automatically clear mock calls, instances, contexts and results before every test. ' +
'Equivalent to calling jest.clearAllMocks() before each test.',
type: 'boolean',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Array [
},
Object {
"initial": false,
"message": "Automatically clear mock calls, instances and results before every test?",
"message": "Automatically clear mock calls, instances, contexts and results before every test?",
"name": "clearMocks",
"type": "confirm",
},
Expand All @@ -131,7 +131,7 @@ module.exports = {
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest",
// Automatically clear mock calls, instances and results before every test
// Automatically clear mock calls, instances, contexts and results before every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-cli/src/init/questions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const defaultQuestions: Array<PromptObject> = [
{
initial: false,
message:
'Automatically clear mock calls, instances and results before every test?',
'Automatically clear mock calls, instances, contexts and results before every test?',
name: 'clearMocks',
type: 'confirm',
},
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-config/src/Descriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const descriptions: {[key in keyof Config.InitialOptions]: string} = {
cacheDirectory:
'The directory where Jest should store its cached dependency information',
clearMocks:
'Automatically clear mock calls, instances and results before every test',
'Automatically clear mock calls, instances, contexts and results before every test',
collectCoverage:
'Indicates whether the coverage information should be collected while executing the test',
collectCoverageFrom:
Expand Down
2 changes: 1 addition & 1 deletion packages/jest-environment/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export interface Jest {
*/
autoMockOn(): Jest;
/**
* Clears the `mock.calls`, `mock.instances` and `mock.results` properties of
* Clears the `mock.calls`, `mock.instances`, `mock.contexts` and `mock.results` properties of
* all mocks. Equivalent to calling `.mockClear()` on every mocked function.
*/
clearAllMocks(): Jest;
Expand Down
5 changes: 4 additions & 1 deletion packages/jest-mock/__typetests__/mock-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ expectType<Mock<(e: any) => never>>(
);
expectError(fn('moduleName'));

const mockFn = fn((a: string, b?: number) => true);
declare const mockFnImpl: (this: Date, a: string, b?: number) => boolean;
const mockFn = fn(mockFnImpl);
const mockAsyncFn = fn(async (p: boolean) => 'value');

expectType<boolean>(mockFn('one', 2));
Expand Down Expand Up @@ -135,6 +136,8 @@ if (returnValue.type === 'throw') {
expectType<unknown>(returnValue.value);
}

expectType<Array<Date>>(mockFn.mock.contexts);

expectType<Mock<(a: string, b?: number | undefined) => boolean>>(
mockFn.mockClear(),
);
Expand Down
33 changes: 33 additions & 0 deletions packages/jest-mock/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,20 +424,53 @@ describe('moduleMocker', () => {
expect(fn.mock.instances[1]).toBe(instance2);
});

it('tracks context objects passed to mock calls', () => {
const fn = moduleMocker.fn();
expect(fn.mock.instances).toEqual([]);

const ctx0 = {};
fn.apply(ctx0, []);
expect(fn.mock.contexts[0]).toBe(ctx0);

const ctx1 = {};
fn.call(ctx1);
expect(fn.mock.contexts[1]).toBe(ctx1);

const ctx2 = {};
const bound2 = fn.bind(ctx2);
bound2();
expect(fn.mock.contexts[2]).toBe(ctx2);

// null context
fn.apply(null, []);
expect(fn.mock.contexts[3]).toBe(null);
fn.call(null);
expect(fn.mock.contexts[4]).toBe(null);
fn.bind(null)();
expect(fn.mock.contexts[5]).toBe(null);

// Unspecified context is `undefined` in strict mode (like in this test) and `window` otherwise.
fn();
expect(fn.mock.contexts[6]).toBe(undefined);
});

it('supports clearing mock calls', () => {
const fn = moduleMocker.fn();
expect(fn.mock.calls).toEqual([]);

fn(1, 2, 3);
expect(fn.mock.calls).toEqual([[1, 2, 3]]);
expect(fn.mock.contexts).toEqual([undefined]);

fn.mockReturnValue('abcd');

fn.mockClear();
expect(fn.mock.calls).toEqual([]);
expect(fn.mock.contexts).toEqual([]);

fn('a', 'b', 'c');
expect(fn.mock.calls).toEqual([['a', 'b', 'c']]);
expect(fn.mock.contexts).toEqual([undefined]);

expect(fn()).toEqual('abcd');
});
Expand Down
6 changes: 6 additions & 0 deletions packages/jest-mock/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ type MockFunctionState<T extends FunctionLike = UnknownFunction> = {
* List of all the object instances that have been instantiated from the mock.
*/
instances: Array<ReturnType<T>>;
/**
* List of all the function contexts that have been applied to calls to the mock.
*/
contexts: Array<ThisParameterType<T>>;
/**
* List of the call order indexes of the mock. Jest is indexing the order of
* invocations of all mocks in a test file. The index is starting with `1`.
Expand Down Expand Up @@ -569,6 +573,7 @@ export class ModuleMocker {
private _defaultMockState(): MockFunctionState {
return {
calls: [],
contexts: [],
instances: [],
invocationCallOrder: [],
results: [],
Expand Down Expand Up @@ -636,6 +641,7 @@ export class ModuleMocker {
const mockState = mocker._ensureMockState(f);
const mockConfig = mocker._ensureMockConfig(f);
mockState.instances.push(this);
mockState.contexts.push(this);
mockState.calls.push(args);
// Create and record an "incomplete" mock result immediately upon
// calling rather than waiting for the mock to return. This avoids
Expand Down

0 comments on commit f496a93

Please sign in to comment.