From b07ff933fa87ddb1eee8f5cc8fa6e952a1ef4c0e Mon Sep 17 00:00:00 2001 From: mrazauskas <72159681+mrazauskas@users.noreply.github.com> Date: Tue, 22 Feb 2022 21:01:18 +0200 Subject: [PATCH] add mock function type tests --- .../__typetests__/mock-functions.test.ts | 282 ++++++++++++++++++ .../jest-mock/__typetests__/tsconfig.json | 11 + packages/jest-mock/package.json | 18 +- packages/jest-mock/src/index.ts | 72 +++-- .../jest-types/__typetests__/jest.test.ts | 140 +++------ yarn.lock | 6 +- 6 files changed, 394 insertions(+), 135 deletions(-) create mode 100644 packages/jest-mock/__typetests__/mock-functions.test.ts create mode 100644 packages/jest-mock/__typetests__/tsconfig.json diff --git a/packages/jest-mock/__typetests__/mock-functions.test.ts b/packages/jest-mock/__typetests__/mock-functions.test.ts new file mode 100644 index 000000000000..56c9c0f46e44 --- /dev/null +++ b/packages/jest-mock/__typetests__/mock-functions.test.ts @@ -0,0 +1,282 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { + expectAssignable, + expectError, + expectNotAssignable, + expectType, +} from 'tsd-lite'; +import {Mock, SpyInstance, fn, spyOn} from 'jest-mock'; + +// jest.fn() + +expectAssignable(fn()); // eslint-disable-line @typescript-eslint/ban-types + +expectType>(fn()); +expectType>(fn(() => {})); +expectType>( + fn((a: string, b?: number) => true), +); +expectType>( + fn((e: any) => { + throw new Error(); + }), +); +expectError(fn('moduleName')); + +const mockFn = fn((a: string, b?: number) => true); +const mockAsyncFn = fn(async (p: boolean) => 'value'); + +expectType(mockFn('one', 2)); +expectType>(mockAsyncFn(false)); +expectError(mockFn()); +expectError(mockAsyncFn()); + +const MockObject = fn((credentials: string) => ({ + connect() { + return fn(); + }, + disconnect() { + return; + }, +})); + +expectType<{ + connect(): Mock>; + disconnect(): void; +}>(new MockObject('credentials')); + +expectError(new MockObject()); + +expectType(mockFn.getMockName()); +expectError(mockFn.getMockName('some-mock')); + +expectType(mockFn.mock.calls.length); + +expectType(mockFn.mock.calls[0][0]); +expectType(mockFn.mock.calls[0][1]); + +expectType(mockFn.mock.calls[1][0]); +expectType(mockFn.mock.calls[1][1]); + +expectType<[a: string, b?: number | undefined] | undefined>( + mockFn.mock.lastCall, +); + +expectType>(mockFn.mock.invocationCallOrder); + +expectType< + Array<{ + connect(): Mock>; + disconnect(): void; + }> +>(MockObject.mock.instances); + +const returnValue = mockFn.mock.results[0]; + +expectType<'incomplete' | 'return' | 'throw'>(returnValue.type); +expectType(returnValue.value); + +if (returnValue.type === 'incomplete') { + expectType(returnValue.value); +} + +if (returnValue.type === 'return') { + expectType(returnValue.value); +} + +if (returnValue.type === 'throw') { + expectType(returnValue.value); +} + +expectType>( + mockFn.mockClear(), +); +expectError(mockFn.mockClear('some-mock')); + +expectType>( + mockFn.mockReset(), +); +expectError(mockFn.mockClear('some-mock')); + +expectType(mockFn.mockRestore()); +expectError(mockFn.mockClear('some-mock')); + +expectType>( + mockFn.mockImplementation((a, b) => { + expectType(a); + expectType(b); + return false; + }), +); +expectError(mockFn.mockImplementation((a: number) => false)); +expectError(mockFn.mockImplementation(a => 'false')); +expectError(mockFn.mockImplementation()); + +expectType, [p: boolean]>>( + mockAsyncFn.mockImplementation(async a => { + expectType(a); + return 'mock value'; + }), +); +expectError(mockAsyncFn.mockImplementation(a => 'mock value')); + +expectType>( + mockFn.mockImplementationOnce((a, b) => { + expectType(a); + expectType(b); + return false; + }), +); +expectError(mockFn.mockImplementationOnce((a: number) => false)); +expectError(mockFn.mockImplementationOnce(a => 'false')); +expectError(mockFn.mockImplementationOnce()); + +expectType, [p: boolean]>>( + mockAsyncFn.mockImplementationOnce(async a => { + expectType(a); + return 'mock value'; + }), +); +expectError(mockAsyncFn.mockImplementationOnce(a => 'mock value')); + +expectType>( + mockFn.mockName('mockedFunction'), +); +expectError(mockFn.mockName(123)); +expectError(mockFn.mockName()); + +expectType>( + mockFn.mockReturnThis(), +); +expectError(mockFn.mockReturnThis('this')); + +expectType>( + mockFn.mockReturnValue(false), +); +expectError(mockFn.mockReturnValue('true')); +expectError(mockFn.mockReturnValue()); + +expectType, [p: boolean]>>( + mockAsyncFn.mockReturnValue(Promise.resolve('mock value')), +); +expectError(mockAsyncFn.mockReturnValue(Promise.resolve(true))); + +expectType>( + mockFn.mockReturnValueOnce(false), +); +expectError(mockFn.mockReturnValueOnce('true')); +expectError(mockFn.mockReturnValueOnce()); + +expectType, [p: boolean]>>( + mockAsyncFn.mockReturnValueOnce(Promise.resolve('mock value')), +); +expectError(mockAsyncFn.mockReturnValueOnce(Promise.resolve(true))); + +expectType, []>>( + fn(() => Promise.resolve('')).mockResolvedValue('Mock value'), +); +expectError(fn(() => Promise.resolve('')).mockResolvedValue(123)); +expectError(fn(() => Promise.resolve('')).mockResolvedValue()); + +expectType, []>>( + fn(() => Promise.resolve('')).mockResolvedValueOnce('Mock value'), +); +expectError(fn(() => Promise.resolve('')).mockResolvedValueOnce(123)); +expectError(fn(() => Promise.resolve('')).mockResolvedValueOnce()); + +expectType, []>>( + fn(() => Promise.resolve('')).mockRejectedValue(new Error('Mock error')), +); +expectType, []>>( + fn(() => Promise.resolve('')).mockRejectedValue('Mock error'), +); +expectError(fn(() => Promise.resolve('')).mockRejectedValue()); + +expectType, []>>( + fn(() => Promise.resolve('')).mockRejectedValueOnce(new Error('Mock error')), +); +expectType, []>>( + fn(() => Promise.resolve('')).mockRejectedValueOnce('Mock error'), +); +expectError(fn(() => Promise.resolve('')).mockRejectedValueOnce()); + +// jest.spyOn() + +const spiedArray = ['a', 'b']; + +const spiedFunction = () => {}; + +const spiedObject = { + _propertyB: false, + + methodA() { + return true; + }, + methodB(a: string, b: number) { + return; + }, + methodC(e: any) { + throw new Error(); + }, + + propertyA: 'abc', + + set propertyB(value) { + this._propertyB = value; + }, + get propertyB() { + return this._propertyB; + }, +}; + +const spy = spyOn(spiedObject, 'methodA'); + +expectNotAssignable(spy); // eslint-disable-line @typescript-eslint/ban-types +expectError(spy()); +expectError(new spy()); + +expectType>(spyOn(spiedObject, 'methodA')); +expectType>( + spyOn(spiedObject, 'methodB'), +); +expectType>(spyOn(spiedObject, 'methodC')); + +expectType>(spyOn(spiedObject, 'propertyB', 'get')); +expectType>( + spyOn(spiedObject, 'propertyB', 'set'), +); +expectError(spyOn(spiedObject, 'propertyB')); +expectError(spyOn(spiedObject, 'methodB', 'get')); +expectError(spyOn(spiedObject, 'methodB', 'set')); + +expectType>(spyOn(spiedObject, 'propertyA', 'get')); +expectType>(spyOn(spiedObject, 'propertyA', 'set')); +expectError(spyOn(spiedObject, 'propertyA')); + +expectError(spyOn(spiedObject, 'notThere')); +expectError(spyOn('abc', 'methodA')); +expectError(spyOn(123, 'methodA')); +expectError(spyOn(true, 'methodA')); +expectError(spyOn(spiedObject)); +expectError(spyOn()); + +expectType>( + spyOn(spiedArray as unknown as ArrayConstructor, 'isArray'), +); +expectError(spyOn(spiedArray, 'isArray')); + +expectType>( + spyOn(spiedFunction as unknown as Function, 'toString'), // eslint-disable-line @typescript-eslint/ban-types +); +expectError(spyOn(spiedFunction, 'toString')); + +expectType>( + spyOn(globalThis, 'Date'), +); +expectType>(spyOn(Date, 'now')); diff --git a/packages/jest-mock/__typetests__/tsconfig.json b/packages/jest-mock/__typetests__/tsconfig.json new file mode 100644 index 000000000000..fe8eab794254 --- /dev/null +++ b/packages/jest-mock/__typetests__/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.json", + "compilerOptions": { + "noUnusedLocals": false, + "noUnusedParameters": false, + "skipLibCheck": true, + + "types": [] + }, + "include": ["./**/*"] +} diff --git a/packages/jest-mock/package.json b/packages/jest-mock/package.json index 729724533417..ff2ad63f5a9c 100644 --- a/packages/jest-mock/package.json +++ b/packages/jest-mock/package.json @@ -6,13 +6,6 @@ "url": "https://github.com/facebook/jest.git", "directory": "packages/jest-mock" }, - "engines": { - "node": "^12.13.0 || ^14.15.0 || ^16.13.0 || >=17.0.0" - }, - "dependencies": { - "@jest/types": "^28.0.0-alpha.4", - "@types/node": "*" - }, "license": "MIT", "main": "./build/index.js", "types": "./build/index.d.ts", @@ -23,6 +16,17 @@ }, "./package.json": "./package.json" }, + "dependencies": { + "@jest/types": "^28.0.0-alpha.3", + "@types/node": "*" + }, + "devDependencies": { + "@tsd/typescript": "~4.5.5", + "tsd-lite": "^0.5.1" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || ^16.13.0 || >=17.0.0" + }, "publishConfig": { "access": "public" } diff --git a/packages/jest-mock/src/index.ts b/packages/jest-mock/src/index.ts index 793943cec8eb..0e3d7e8bfb82 100644 --- a/packages/jest-mock/src/index.ts +++ b/packages/jest-mock/src/index.ts @@ -122,8 +122,10 @@ export interface MockInstance> { mockReset(): this; mockRestore(): void; mockImplementation(fn: (...args: Y) => T): this; + /** @internal */ mockImplementation(fn: () => Promise): this; mockImplementationOnce(fn: (...args: Y) => T): this; + /** @internal */ mockImplementationOnce(fn: () => Promise): this; mockName(name: string): this; mockReturnThis(): this; @@ -137,43 +139,58 @@ export interface MockInstance> { type Unpromisify = T extends Promise ? R : never; -/** - * Possible types of a MockFunctionResult. - * 'return': The call completed by returning normally. - * 'throw': The call completed by throwing a value. - * 'incomplete': The call has not completed yet. This is possible if you read - * the mock function result from within the mock function itself - * (or a function called by the mock function). - */ -type MockFunctionResultType = 'return' | 'throw' | 'incomplete'; - -/** - * Represents the result of a single call to a mock function. - */ -type MockFunctionResult = { +type MockFunctionResultIncomplete = { + type: 'incomplete'; /** - * Indicates how the call completed. + * Result of a single call to a mock function that has not yet completed. + * This occurs if you test the result from within the mock function itself, + * or from within a function that was called by the mock. */ - type: MockFunctionResultType; + value: undefined; +}; +type MockFunctionResultReturn = { + type: 'return'; /** - * The value that was either thrown or returned by the function. - * Undefined when type === 'incomplete'. + * Result of a single call to a mock function that returned. + */ + value: T; +}; +type MockFunctionResultThrow = { + type: 'throw'; + /** + * Result of a single call to a mock function that threw. */ value: unknown; }; +type MockFunctionResult = + | MockFunctionResultIncomplete + | MockFunctionResultReturn + | MockFunctionResultThrow; + type MockFunctionState> = { + /** + * List of the call arguments of all calls that have been made to the mock. + */ calls: Array; + /** + * List of all the object instances that have been instantiated from the mock. + */ instances: Array; + /** + * 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`. + */ invocationCallOrder: Array; /** - * Getter for retrieving the last call arguments + * List of the call arguments of the last call that was made to the mock. + * If the function was not called, it will return `undefined`. */ lastCall?: Y; /** - * List of results of calls to the mock function. + * List of the results of all calls that have been made to the mock. */ - results: Array; + results: Array>; }; type MockFunctionConfig = { @@ -615,7 +632,7 @@ export class ModuleMocker { // calling rather than waiting for the mock to return. This avoids // issues caused by recursion where results can be recorded in the // wrong order. - const mockResult: MockFunctionResult = { + const mockResult: MockFunctionResult = { type: 'incomplete', value: undefined, }; @@ -686,6 +703,7 @@ export class ModuleMocker { // NOTE: Intentionally NOT pushing/indexing into the array of mock // results here to avoid corrupting results data if mockClear() // is called during the execution of the mock. + // @ts-expect-error reassigning 'incomplete' mockResult.type = callDidThrowError ? 'throw' : 'return'; mockResult.value = callDidThrowError ? thrownError : finalReturnValue; } @@ -992,12 +1010,18 @@ export class ModuleMocker { return metadata; } + isMockFunction = Array>( + fn: Mock, + ): fn is Mock; + isMockFunction = Array>( + fn: SpyInstance, + ): fn is SpyInstance; isMockFunction = Array>( fn: (...args: Y) => T, ): fn is Mock; isMockFunction(fn: unknown): fn is Mock; isMockFunction(fn: unknown): fn is Mock { - return !!fn && (fn as any)._isMockFunction === true; + return !!fn && (fn as Mock)._isMockFunction === true; } fn>( @@ -1023,7 +1047,7 @@ export class ModuleMocker { accessType: 'set', ): SpyInstance; - spyOn>>( + spyOn>( object: T, methodName: M, ): T[M] extends new (...args: Array) => any diff --git a/packages/jest-types/__typetests__/jest.test.ts b/packages/jest-types/__typetests__/jest.test.ts index 8759043aeca6..7dceb9589aa3 100644 --- a/packages/jest-types/__typetests__/jest.test.ts +++ b/packages/jest-types/__typetests__/jest.test.ts @@ -7,7 +7,7 @@ import {expectError, expectType} from 'tsd-lite'; import {jest} from '@jest/globals'; -import type {Mock, SpyInstance} from 'jest-mock'; +import type {Mock, ModuleMocker, SpyInstance, fn, spyOn} from 'jest-mock'; expectType( jest @@ -121,6 +121,12 @@ expectError(jest.unmock()); expectType(jest.clearAllMocks()); expectError(jest.clearAllMocks('moduleName')); +expectType(jest.resetAllMocks()); +expectError(jest.resetAllMocks(true)); + +expectType(jest.restoreAllMocks()); +expectError(jest.restoreAllMocks(false)); + expectType(jest.isMockFunction(() => {})); expectError(jest.isMockFunction()); @@ -150,128 +156,58 @@ if (!jest.isMockFunction(surelyMock)) { expectType(surelyMock); } -declare const stringMaybeMock: string; +const spiedObject = { + methodA(a: number, b: string) { + return true; + }, +}; -if (!jest.isMockFunction(stringMaybeMock)) { - expectType(stringMaybeMock); +const surelySpy = jest.spyOn(spiedObject, 'methodA'); + +if (jest.isMockFunction(surelySpy)) { + expectType>(surelySpy); + + surelySpy.mockReturnValueOnce(false); + expectError(surelyMock.mockReturnValueOnce(123)); +} + +if (!jest.isMockFunction(surelySpy)) { + expectType(surelySpy); } +declare const stringMaybeMock: string; + if (jest.isMockFunction(stringMaybeMock)) { expectType>>(stringMaybeMock); } -declare const anyMaybeMock: any; - -if (!jest.isMockFunction(anyMaybeMock)) { - expectType(anyMaybeMock); +if (!jest.isMockFunction(stringMaybeMock)) { + expectType(stringMaybeMock); } +declare const anyMaybeMock: any; + if (jest.isMockFunction(anyMaybeMock)) { expectType>>(anyMaybeMock); } -declare const unknownMaybeMock: unknown; - -if (!jest.isMockFunction(unknownMaybeMock)) { - expectType(unknownMaybeMock); +if (!jest.isMockFunction(anyMaybeMock)) { + expectType(anyMaybeMock); } +declare const unknownMaybeMock: unknown; + if (jest.isMockFunction(unknownMaybeMock)) { expectType>>(unknownMaybeMock); } -expectType>(jest.fn()); -expectType>(jest.fn(() => {})); -expectType>( - jest.fn((a: string, b: number) => true), -); -expectType>( - jest.fn((e: any) => { - throw new Error(); - }), -); -expectError(jest.fn('moduleName')); - -expectType(jest.resetAllMocks()); -expectError(jest.resetAllMocks(true)); - -expectType(jest.restoreAllMocks()); -expectError(jest.restoreAllMocks(false)); - -const spiedArray = ['a', 'b']; - -const spiedFunction = () => {}; - -spiedFunction.toString(); - -const spiedObject = { - _propertyB: false, - - methodA() { - return true; - }, - methodB(a: string, b: number) { - return; - }, - methodC(e: any) { - throw new Error(); - }, - - propertyA: 'abc', - - set propertyB(value) { - this._propertyB = value; - }, - get propertyB() { - return this._propertyB; - }, -}; - -expectType>(jest.spyOn(spiedObject, 'methodA')); -expectType>( - jest.spyOn(spiedObject, 'methodB'), -); -expectType>(jest.spyOn(spiedObject, 'methodC')); - -expectType>( - jest.spyOn(spiedObject, 'propertyB', 'get'), -); -expectType>( - jest.spyOn(spiedObject, 'propertyB', 'set'), -); -expectError(jest.spyOn(spiedObject, 'propertyB')); -expectError(jest.spyOn(spiedObject, 'methodB', 'get')); -expectError(jest.spyOn(spiedObject, 'methodB', 'set')); - -expectType>( - jest.spyOn(spiedObject, 'propertyA', 'get'), -); -expectType>( - jest.spyOn(spiedObject, 'propertyA', 'set'), -); -expectError(jest.spyOn(spiedObject, 'propertyA')); - -expectError(jest.spyOn(spiedObject, 'notThere')); -expectError(jest.spyOn('abc', 'methodA')); -expectError(jest.spyOn(123, 'methodA')); -expectError(jest.spyOn(true, 'methodA')); -expectError(jest.spyOn(spiedObject)); -expectError(jest.spyOn()); - -expectType>( - jest.spyOn(spiedArray as unknown as ArrayConstructor, 'isArray'), -); -expectError(jest.spyOn(spiedArray, 'isArray')); +if (!jest.isMockFunction(unknownMaybeMock)) { + expectType(unknownMaybeMock); +} -expectType>( - jest.spyOn(spiedFunction as unknown as Function, 'toString'), // eslint-disable-line @typescript-eslint/ban-types -); -expectError(jest.spyOn(spiedFunction, 'toString')); +expectType(jest.fn); -expectType>( - jest.spyOn(globalThis, 'Date'), -); -expectType>(jest.spyOn(Date, 'now')); +expectType(jest.spyOn); // Mock Timers diff --git a/yarn.lock b/yarn.lock index d468e9806ddf..2e80d002caa6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2858,7 +2858,7 @@ __metadata: languageName: unknown linkType: soft -"@jest/types@^28.0.0-alpha.4, @jest/types@workspace:packages/jest-types": +"@jest/types@^28.0.0-alpha.3, @jest/types@^28.0.0-alpha.4, @jest/types@workspace:packages/jest-types": version: 0.0.0-use.local resolution: "@jest/types@workspace:packages/jest-types" dependencies: @@ -13210,8 +13210,10 @@ __metadata: version: 0.0.0-use.local resolution: "jest-mock@workspace:packages/jest-mock" dependencies: - "@jest/types": ^28.0.0-alpha.4 + "@jest/types": ^28.0.0-alpha.3 + "@tsd/typescript": ~4.5.5 "@types/node": "*" + tsd-lite: ^0.5.1 languageName: unknown linkType: soft