From 7d8c313f0191a13b83a6433e5f73c9e786549579 Mon Sep 17 00:00:00 2001 From: Tom Mrazauskas Date: Sat, 13 Aug 2022 13:50:16 +0300 Subject: [PATCH] =?UTF-8?q?refactor(jest-mock)!:=20change=20the=20default?= =?UTF-8?q?=20`jest.mocked`=20helper=E2=80=99s=20behaviour=20to=20deep=20m?= =?UTF-8?q?ocked=20(#13125)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc.cjs | 2 +- CHANGELOG.md | 1 + docs/JestObjectAPI.md | 60 +++++-------------- docs/MockFunctionAPI.md | 53 +++++++++++++++- docs/UpgradingToJest29.md | 36 +++++++++-- packages/jest-environment/src/index.ts | 10 ++-- packages/jest-mock/src/index.ts | 13 ++-- .../jest-types/__typetests__/jest.test.ts | 21 ++++--- 8 files changed, 129 insertions(+), 67 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 683a6424e1ac..3a445ce334a1 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -180,7 +180,7 @@ module.exports = { }, }, { - files: ['website/**/*'], + files: ['docs/**/*', 'website/**/*'], rules: { 'import/order': 'off', 'import/sort-keys': 'off', diff --git a/CHANGELOG.md b/CHANGELOG.md index c59948435fde..b3478f5ba123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `[jest-environment-jsdom]` [**BREAKING**] Upgrade to `jsdom@20` ([#13037](https://github.com/facebook/jest/pull/13037), [#13058](https://github.com/facebook/jest/pull/13058)) - `[@jest/globals]` Add `jest.Mocked`, `jest.MockedClass`, `jest.MockedFunction` and `jest.MockedObject` utility types ([#12727](https://github.com/facebook/jest/pull/12727)) - `[jest-mock]` [**BREAKING**] Refactor `Mocked*` utility types. `MaybeMockedDeep` and `MaybeMocked` became `Mocked` and `MockedShallow` respectively; only deep mocked variants of `MockedClass`, `MockedFunction` and `MockedObject` are exported ([#13123](https://github.com/facebook/jest/pull/13123), [#13124](https://github.com/facebook/jest/pull/13124)) +- `[jest-mock]` [**BREAKING**] Change the default `jest.mocked` helper’s behavior to deep mocked ([#13125](https://github.com/facebook/jest/pull/13125)) - `[jest-worker]` Adds `workerIdleMemoryLimit` option which is used as a check for worker memory leaks >= Node 16.11.0 and recycles child workers as required. ([#13056](https://github.com/facebook/jest/pull/13056), [#13105](https://github.com/facebook/jest/pull/13105), [#13106](https://github.com/facebook/jest/pull/13106), [#13107](https://github.com/facebook/jest/pull/13107)) - `[pretty-format]` [**BREAKING**] Remove `ConvertAnsi` plugin in favour of `jest-serializer-ansi-escapes` ([#13040](https://github.com/facebook/jest/pull/13040)) diff --git a/docs/JestObjectAPI.md b/docs/JestObjectAPI.md index c68f4d009133..add6b8c49a6c 100644 --- a/docs/JestObjectAPI.md +++ b/docs/JestObjectAPI.md @@ -282,6 +282,20 @@ Modules that are mocked with `jest.mock` are mocked only for the file that calls Returns the `jest` object for chaining. +:::tip + +Writing tests in TypeScript? Use [`jest.Mocked`](MockFunctionAPI.md/#jestmockedsource) utility type or [`jest.mocked()`](MockFunctionAPI.md/#jestmockedsource-options) helper method to have your mocked modules typed. + +::: + +### `jest.Mocked` + +See [TypeScript Usage](MockFunctionAPI.md/#jestmockedsource) chapter of Mock Functions page for documentation. + +### `jest.mocked(source, options?)` + +See [TypeScript Usage](MockFunctionAPI.md/#jestmockedsource-options) chapter of Mock Functions page for documentation. + ### `jest.unmock(moduleName)` Indicates that the module system should never return a mocked version of the specified module from `require()` (e.g. that it should always return the real module). @@ -467,7 +481,7 @@ const returnsTrue = jest.fn(() => true); console.log(returnsTrue()); // true; ``` -:::note +:::tip See [Mock Functions](MockFunctionAPI.md#jestfnimplementation) page for details on TypeScript usage. @@ -598,50 +612,6 @@ Returns the `jest` object for chaining. Restores all mocks back to their original value. Equivalent to calling [`.mockRestore()`](MockFunctionAPI.md#mockfnmockrestore) on every mocked function. Beware that `jest.restoreAllMocks()` only works when the mock was created with `jest.spyOn`; other mocks will require you to manually restore them. -### `jest.mocked(item: T, deep = false)` - -The `mocked` test helper provides typings on your mocked modules and even their deep methods, based on the typing of its source. It makes use of the latest TypeScript feature, so you even have argument types completion in the IDE (as opposed to `jest.MockInstance`). - -_Note: while it needs to be a function so that input type is changed, the helper itself does nothing else than returning the given input value._ - -Example: - -```ts -// foo.ts -export const foo = { - a: { - b: { - c: { - hello: (name: string) => `Hello, ${name}`, - }, - }, - }, - name: () => 'foo', -}; -``` - -```ts -// foo.spec.ts -import {foo} from './foo'; -jest.mock('./foo'); - -// here the whole foo var is mocked deeply -const mockedFoo = jest.mocked(foo, true); - -test('deep', () => { - // there will be no TS error here, and you'll have completion in modern IDEs - mockedFoo.a.b.c.hello('me'); - // same here - expect(mockedFoo.a.b.c.hello.mock.calls).toHaveLength(1); -}); - -test('direct', () => { - foo.name(); - // here only foo.name is mocked (or its methods if it's an object) - expect(jest.mocked(foo.name).mock.calls).toHaveLength(1); -}); -``` - ## Fake Timers ### `jest.useFakeTimers(fakeTimersConfig?)` diff --git a/docs/MockFunctionAPI.md b/docs/MockFunctionAPI.md index 0b2fea48b464..526110167a84 100644 --- a/docs/MockFunctionAPI.md +++ b/docs/MockFunctionAPI.md @@ -526,13 +526,17 @@ test('calculate calls add', () => { The `jest.Mocked` utility type returns the `Source` type wrapped with type definitions of Jest mock function. ```ts -import fetch from 'node-fetch'; import {expect, jest, test} from '@jest/globals'; +import type {fetch} from 'node-fetch'; jest.mock('node-fetch'); let mockedFetch: jest.Mocked; +afterEach(() => { + mockedFetch.mockClear(); +}); + test('makes correct call', () => { mockedFetch = getMockedFetch(); // ... @@ -545,3 +549,50 @@ test('returns correct data', () => { ``` Types of classes, functions or objects can be passed as type argument to `jest.Mocked`. If you prefer to constrain the input type, use: `jest.MockedClass`, `jest.MockedFunction` or `jest.MockedObject`. + +### `jest.mocked(source, options?)` + +The `mocked()` helper method wraps types of the `source` object and its deep nested members with type definitions of Jest mock function. You can pass `{shallow: true}` as the `options` argument to disable the deeply mocked behavior. + +Returns the `source` object. + +```ts title="song.ts" +export const song = { + one: { + more: { + time: (t: number) => { + return t; + }, + }, + }, +}; +``` + +```ts title="song.test.ts" +import {expect, jest, test} from '@jest/globals'; +import {song} from './song'; + +jest.mock('./song'); +jest.spyOn(console, 'log'); + +const mockedSong = jest.mocked(song); +// or through `jest.Mocked` +// const mockedSong = song as jest.Mocked; + +test('deep method is typed correctly', () => { + mockedSong.one.more.time.mockReturnValue(12); + + expect(mockedSong.one.more.time(10)).toBe(12); + expect(mockedSong.one.more.time.mock.calls).toHaveLength(1); +}); + +test('direct usage', () => { + jest.mocked(console.log).mockImplementation(() => { + return; + }); + + console.log('one more time'); + + expect(jest.mocked(console.log).mock.calls).toHaveLength(1); +}); +``` diff --git a/docs/UpgradingToJest29.md b/docs/UpgradingToJest29.md index 2dad8da9854f..89dacba4beca 100644 --- a/docs/UpgradingToJest29.md +++ b/docs/UpgradingToJest29.md @@ -40,10 +40,38 @@ If you want to keep the old behavior, you can set the `snapshotFormat` property Notably, `jsdom@20` includes support for `crypto.getRandomValues()`, which means packages like `jsdom` and `nanoid`, which doesn't work properly in Jest@28, can work without extra polyfills. -## `jest-mock` +## `pretty-format` -Exports of `Mocked*` utility types changed. `MaybeMockedDeep` and `MaybeMocked` now are exported as `Mocked` and `MockedShallow` respectively; only deep mocked variants of `MockedClass`, `MockedFunction` and `MockedObject` are exposed. +`ConvertAnsi` plugin is removed from `pretty-format` package in favour of [`jest-serializer-ansi-escapes`](https://github.com/mrazauskas/jest-serializer-ansi-escapes). -## `pretty-format` +### `jest-mock` + +Exports of `Mocked*` utility types from `jest-mock` package have changed. `MaybeMockedDeep` and `MaybeMocked` now are exported as `Mocked` and `MockedShallow` respectively; only deep mocked variants of `MockedClass`, `MockedFunction` and `MockedObject` are exposed. + +## TypeScript + +:::info + +The TypeScript examples from this page will only work as documented if you import `jest` from `'@jest/globals'`: -`ConvertAnsi` plugin is removed in favour of [`jest-serializer-ansi-escapes`](https://github.com/mrazauskas/jest-serializer-ansi-escapes). +```ts +import {jest} from '@jest/globals'; +``` + +::: + +### `jest.mocked()` + +The [`jest.mocked()`](MockFunctionAPI.md/#jestmockedsource-options) helper method now wraps types of deep members of passed object by default. If you have used the method with `true` as the second argument, remove it to avoid type errors: + +```diff +- const mockedObject = jest.mocked(someObject, true); ++ const mockedObject = jest.mocked(someObject); +``` + +To have the old shallow mocked behavior, pass `{shallow: true}` as the second argument: + +```diff +- const mockedObject = jest.mocked(someObject); ++ const mockedObject = jest.mocked(someObject, {shallow: true}); +``` diff --git a/packages/jest-environment/src/index.ts b/packages/jest-environment/src/index.ts index 7a6e1cc8f7ef..4d4df64eaad7 100644 --- a/packages/jest-environment/src/index.ts +++ b/packages/jest-environment/src/index.ts @@ -201,6 +201,12 @@ export interface Jest { ``` */ requireActual: (moduleName: string) => unknown; + /** + * Wraps types of the `source` object and its deep members with type definitions + * of Jest mock function. Pass `{shallow: true}` option to disable the deeply + * mocked behavior. + */ + mocked: ModuleMocker['mocked']; /** * Returns a mock module instead of the actual module, bypassing all checks * on whether the module should be required normally or not. @@ -224,10 +230,6 @@ export interface Jest { * with `jest.spyOn()`; other mocks will require you to manually restore them. */ restoreAllMocks(): Jest; - /** - * Wraps an object or a module with mock type definitions. - */ - mocked: ModuleMocker['mocked']; /** * Runs failed tests n-times until they pass or until the max number of * retries is exhausted. diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index eac96db050d6..305699b0a50c 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -1216,13 +1216,16 @@ export class ModuleMocker { return value == null ? `${value}` : typeof value; } - mocked(item: T, deep?: false): MockedShallow; - mocked(item: T, deep: true): Mocked; + mocked(source: T, options?: {shallow: false}): Mocked; mocked( - item: T, - _deep = false, + source: T, + options: {shallow: true}, + ): MockedShallow; + mocked( + source: T, + _options?: {shallow: boolean}, ): Mocked | MockedShallow { - return item as Mocked | MockedShallow; + return source as Mocked | MockedShallow; } } diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index f5f12ba68f05..997aad9cac69 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -13,6 +13,7 @@ import type { MockedClass, MockedFunction, MockedObject, + MockedShallow, ModuleMocker, SpyInstance, } from 'jest-mock'; @@ -276,11 +277,19 @@ expectType>( someObject as jest.MockedObject, ); -// deep mocked() +// mocked() -const mockObjectA = jest.mocked(someObject, true); +expectType>(jest.mocked(someObject)); +expectType>( + jest.mocked(someObject, {shallow: false}), +); +expectType>( + jest.mocked(someObject, {shallow: true}), +); -expectError(jest.mocked('abc', true)); +expectError(jest.mocked('abc')); + +const mockObjectA = jest.mocked(someObject); expectType<[]>(mockObjectA.methodA.mock.calls[0]); expectType<[b: string]>(mockObjectA.methodB.mock.calls[0]); @@ -333,11 +342,9 @@ expectError( expectAssignable(mockObjectA); -// mocked() - -const mockObjectB = jest.mocked(someObject); +// shallow mocked() -expectError(jest.mocked('abc')); +const mockObjectB = jest.mocked(someObject, {shallow: true}); expectType<[]>(mockObjectB.methodA.mock.calls[0]); expectType<[b: string]>(mockObjectB.methodB.mock.calls[0]);