Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

### Features

- `[@jest/mock]` Add `withImplementation` method for temporarily overriding a mock.
- `[expect, @jest/expect]` support type inference for function parameters in `CalledWith` assertions ([#13268](https://github.com/facebook/jest/pull/13268))
- `[expect, @jest/expect]` Infer type of `*ReturnedWith` matchers argument ([#13278](https://github.com/facebook/jest/pull/13278))
- `[@jest/environment, jest-runtime]` Allow `jest.requireActual` and `jest.requireMock` to take a type argument ([#13253](https://github.com/facebook/jest/pull/13253))
Expand Down
37 changes: 37 additions & 0 deletions docs/MockFunctionAPI.md
Original file line number Diff line number Diff line change
Expand Up @@ -478,6 +478,43 @@ test('async test', async () => {
});
```

### `mockFn.withImplementation(fn, callback)`

Accepts a function which should be temporarily used as the implementation of the mock while the callback is being executed.

```js
test('test', () => {
const mock = jest.fn(() => 'outside callback');

mock.withImplementation(
() => 'inside callback',
() => {
mock(); // 'inside callback'
},
);

mock(); // 'outside callback'
});
```

`mockFn.withImplementation` can be used regardless of whether or not the callback is asynchronous (returns a promise like object). If the callback is asynchronous a promise will be returned. Awaiting the promise will await the callback and reset the implementation.

```js
test('async test', async () => {
const mock = jest.fn(() => 'outside callback');

// We await this call since the callback is async
await mock.withImplementation(
() => 'inside callback',
async () => {
mock(); // 'inside callback'
},
);

mock(); // 'outside callback'
});
```

## TypeScript Usage

:::tip
Expand Down
6 changes: 6 additions & 0 deletions packages/jest-mock/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,9 @@ In case both `.mockImplementationOnce()` / `.mockImplementation()` and `.mockRet

- if the last call is `.mockReturnValueOnce()` or `.mockReturnValue()`, use the specific return value or default return value. If specific return values are used up or no default return value is set, fall back to try `.mockImplementation()`;
- if the last call is `.mockImplementationOnce()` or `.mockImplementation()`, run the specific implementation and return the result or run default implementation and return the result.

##### `.withImplementation(function, callback)`

Temporarily overrides the default mock implementation within the callback, then restores it's previous implementation.

If the callback is async or returns a promise like object, `withImplementation` will return a promise. Awaiting the promise will await the callback and reset the implementation.
7 changes: 6 additions & 1 deletion packages/jest-mock/__typetests__/mock-functions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,12 @@ expectType<Mock<() => Promise<string>>>(
);
expectError(fn(() => Promise.resolve('')).mockRejectedValueOnce());

// jest.spyOn()
expectType<void>(mockFn.withImplementation(mockFnImpl, () => {}));
expectType<Promise<void>>(
mockFn.withImplementation(mockFnImpl, async () => {}),
);

expectError(mockFn.withImplementation(mockFnImpl, () => {}).then());

const spiedArray = ['a', 'b'];

Expand Down
53 changes: 53 additions & 0 deletions packages/jest-mock/src/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* eslint-disable local/ban-types-eventually, local/prefer-rest-params-eventually */

import * as util from 'util';
import vm, {Context} from 'vm';
import {ModuleMocker, fn, mocked, spyOn} from '../';

Expand Down Expand Up @@ -1073,6 +1074,58 @@ describe('moduleMocker', () => {
});
});

describe('withImplementation', () => {
it('sets an implementation which is available within the callback', async () => {
const mock1 = jest.fn();
const mock2 = jest.fn();

const Module = jest.fn(() => ({someFn: mock1}));
const testFn = function () {
const m = new Module();
m.someFn();
};

Module.withImplementation(
() => ({someFn: mock2}),
() => {
testFn();
expect(mock2).toHaveBeenCalled();
expect(mock1).not.toHaveBeenCalled();
},
);

testFn();
expect(mock1).toHaveBeenCalled();
});

it('returns a promise if the provided callback is asynchronous', async () => {
const mock1 = jest.fn();
const mock2 = jest.fn();

const Module = jest.fn(() => ({someFn: mock1}));
const testFn = function () {
const m = new Module();
m.someFn();
};

const promise = Module.withImplementation(
() => ({someFn: mock2}),
async () => {
testFn();
expect(mock2).toHaveBeenCalled();
expect(mock1).not.toHaveBeenCalled();
},
);

expect(util.types.isPromise(promise)).toBe(true);

await promise;

testFn();
expect(mock1).toHaveBeenCalled();
});
});

test('mockReturnValue does not override mockImplementationOnce', () => {
const mockFn = jest
.fn()
Expand Down
34 changes: 34 additions & 0 deletions packages/jest-mock/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ export interface MockInstance<T extends FunctionLike = UnknownFunction> {
mockRestore(): void;
mockImplementation(fn: T): this;
mockImplementationOnce(fn: T): this;
withImplementation(fn: T, callback: () => Promise<unknown>): Promise<void>;
withImplementation(fn: T, callback: () => void): void;
mockName(name: string): this;
mockReturnThis(): this;
mockReturnValue(value: ReturnType<T>): this;
Expand Down Expand Up @@ -768,6 +770,38 @@ export class ModuleMocker {
return f;
};

f.withImplementation = withImplementation.bind(this);

function withImplementation(fn: T, callback: () => void): void;
function withImplementation(
fn: T,
callback: () => Promise<unknown>,
): Promise<void>;
function withImplementation(
this: ModuleMocker,
fn: T,
callback: (() => void) | (() => Promise<unknown>),
): void | Promise<void> {
// Remember previous mock implementation, then set new one
const mockConfig = this._ensureMockConfig(f);
const previousImplementation = mockConfig.mockImpl;
mockConfig.mockImpl = fn;

const returnedValue = callback();

if (
typeof returnedValue === 'object' &&
returnedValue !== null &&
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
typeof returnedValue === 'object' &&
returnedValue !== null &&
returnedValue != null &&
typeof returnedValue === 'object' &&

probably doesn't matter, but easier to bail out early

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point. Like I mentioned in one of my previous comments I copied this verbatim from jest circus.

So there's probably a nice tiny performance enhancement waiting to be made there as well:
https://github.com/facebook/jest/blob/a20fd859673800c50f8f089cfb4a87faec119525/packages/jest-circus/src/utils.ts#L271-L275

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

haha, the linked SO answer says not to use it 🙈

Copy link
Member

Choose a reason for hiding this comment

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

I opened #13314

typeof returnedValue.then === 'function'
) {
return returnedValue.then(() => {
mockConfig.mockImpl = previousImplementation;
});
} else {
mockConfig.mockImpl = previousImplementation;
}
}

f.mockImplementation = (fn: UnknownFunction) => {
// next function call will use mock implementation return value
const mockConfig = this._ensureMockConfig(f);
Expand Down