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

Add contexts to mocks #12601

Merged
merged 17 commits into from
Mar 29, 2022
Merged
Show file tree
Hide file tree
Changes from 10 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: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@
- `[jest-haste-map]` [**BREAKING**] `HasteMap.create` now returns a promise ([#12008](https://github.com/facebook/jest/pull/12008))
- `[jest-haste-map]` Add support for `dependencyExtractor` written in ESM ([#12008](https://github.com/facebook/jest/pull/12008))
- `[jest-mock]` [**BREAKING**] Rename exported utility types `ClassLike`, `FunctionLike`, `ConstructorLikeKeys`, `MethodLikeKeys`, `PropertyLikeKeys`; remove exports of utility types `ArgumentsOf`, `ArgsType`, `ConstructorArgumentsOf` - TS builtin utility types `ConstructorParameters` and `Parameters` should be used instead ([#12435](https://github.com/facebook/jest/pull/12435), [#12489](https://github.com/facebook/jest/pull/12489))
- `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442))
SimenB marked this conversation as resolved.
Show resolved Hide resolved
- `[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 mocks ([#12601](https://github.com/facebook/jest/pull/12601))- `[jest-mock]` Improve `isMockFunction` to infer types of passed function ([#12442](https://github.com/facebook/jest/pull/12442))
- `[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
27 changes: 26 additions & 1 deletion docs/MockFunctionAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,31 @@ 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,7 +129,7 @@ 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.
matthias-ccri marked this conversation as resolved.
Show resolved Hide resolved

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
6 changes: 5 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);
matthias-ccri marked this conversation as resolved.
Show resolved Hide resolved
declare const mockFnImpl: (this: Date, a: string, b?: number) => true;
const mockFn = fn(mockFnImpl);
const mockAsyncFn = fn(async (p: boolean) => 'value');

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

const context = mockFn.mock.contexts[0];
expectType<Date>(context);
matthias-ccri marked this conversation as resolved.
Show resolved Hide resolved

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