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

feat(@jest/globals, jest-mock): add jest.Spied* utility types #13440

Merged
merged 14 commits into from Oct 16, 2022
12 changes: 10 additions & 2 deletions docs/JestObjectAPI.md
Expand Up @@ -141,7 +141,9 @@ module.exports = {
```js title="__tests__/createMockFromModule.test.js"
const utils = jest.createMockFromModule('../utils');

utils.isAuthorized = jest.fn(secret => secret === 'not wizard');
jest
.spyOn(utils, 'isAuthorized')
.mockImplementation(secret => secret === 'not wizard');
Copy link
Member

Choose a reason for hiding this comment

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

it doesn't make sense to spyOn here - isAuthorized is already a mock function (that's what createMockFromModule does)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ups.. This change should not be here. My bad (;

Copy link
Member

@SimenB SimenB Oct 16, 2022

Choose a reason for hiding this comment

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

instead of utils.isAuthorized = jest.fn(secret => secret === 'not wizard'); it should probably be utils.isAuthorized.mockImplementation(secret => secret === 'not wizard');

Copy link
Contributor Author

Choose a reason for hiding this comment

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

These lines got autocorrected, because I was playing with jest/prefer-spy-on ESLint rule. I was hoping to find better example for docs, but nothing came out. jest/prefer-spy-on rule will come in later as separate PR (;


test('implementation created by jest.createMockFromModule', () => {
expect(jest.isMockFunction(utils.authorize)).toBe(true);
Expand All @@ -162,7 +164,9 @@ export const utils = {
const {utils} =
jest.createMockFromModule<typeof import('../utils')>('../utils');

utils.isAuthorized = jest.fn((secret: string) => secret === 'not wizard');
jest
.spyOn(utils, 'isAuthorized')
.mockImplementation((secret: string) => secret === 'not wizard');

test('implementation created by jest.createMockFromModule', () => {
expect(jest.isMockFunction(utils.authorize)).toBe(true);
Expand Down Expand Up @@ -699,6 +703,10 @@ test('plays audio', () => {
});
```

### `jest.Spied<Source>`

See [TypeScript Usage](MockFunctionAPI.md/#jestspiedsource) chapter of Mock Functions page for documentation.
mrazauskas marked this conversation as resolved.
Show resolved Hide resolved

### `jest.clearAllMocks()`

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.
Expand Down
25 changes: 25 additions & 0 deletions docs/MockFunctionAPI.md
Expand Up @@ -644,3 +644,28 @@ test('direct usage', () => {
expect(jest.mocked(console.log).mock.calls).toHaveLength(1);
});
```

### `jest.Spied<Source>`

Constructs the type of a spied class or function, i.e. the return type of `jest.spyOn()`.

```ts
import {expect, jest, test} from '@jest/globals';
import * as platform from './';

function getLoginSpy(): jest.Spied<typeof platform.login> {
// ...
}

let loginSpy: jest.Spied<typeof platform.login>;
// equivalent to: `ReturnType<typeof jest.spyOn(platform, 'login')> {

test('passes username and password', () => {
loginSpy = getLoginSpy();
// ...
});
```

Types of a class or function can be passed as type argument to `jest.Spied<Source>`. If you prefer to constrain the input type, use: `jest.SpiedClass<Source>` or `jest.SpiedFunction<Source>`.

Use `jest.SpiedGetter<Source>` or `jest.SpiedSetter<Source>` to create the type of a spied getter or setter respectively.
25 changes: 25 additions & 0 deletions packages/jest-globals/src/index.ts
Expand Up @@ -16,6 +16,11 @@ import type {
MockedClass as JestMockedClass,
MockedFunction as JestMockedFunction,
MockedObject as JestMockedObject,
Spied as JestSpied,
SpiedClass as JestSpiedClass,
SpiedFunction as JestSpiedFunction,
SpiedGetter as JestSpiedGetter,
SpiedSetter as JestSpiedSetter,
UnknownFunction,
} from 'jest-mock';

Expand Down Expand Up @@ -58,6 +63,26 @@ declare namespace jest {
* Wraps an object type with Jest mock type definitions.
*/
export type MockedObject<T extends object> = JestMockedObject<T>;
/**
* Constructs the type of a spied class or function.
*/
export type Spied<T extends ClassLike | FunctionLike> = JestSpied<T>;
/**
* Constructs the type of a spied class.
*/
export type SpiedClass<T extends ClassLike> = JestSpiedClass<T>;
/**
* Constructs the type of a spied function.
*/
export type SpiedFunction<T extends FunctionLike> = JestSpiedFunction<T>;
/**
* Constructs the type of a spied getter.
*/
export type SpiedGetter<T> = JestSpiedGetter<T>;
/**
* Constructs the type of a spied setter.
*/
export type SpiedSetter<T> = JestSpiedSetter<T>;
}

export {jest};
Expand Down
74 changes: 42 additions & 32 deletions packages/jest-mock/__typetests__/mock-functions.test.ts
Expand Up @@ -11,7 +11,15 @@ import {
expectNotAssignable,
expectType,
} from 'tsd-lite';
import {Mock, SpyInstance, fn, spyOn} from 'jest-mock';
import {
Mock,
SpiedClass,
SpiedFunction,
SpiedGetter,
SpiedSetter,
fn,
spyOn,
} from 'jest-mock';

// jest.fn()

Expand Down Expand Up @@ -320,26 +328,30 @@ expectNotAssignable<Function>(spy); // eslint-disable-line @typescript-eslint/ba
expectError(spy());
expectError(new spy());

expectType<SpyInstance<typeof spiedObject.methodA>>(
expectType<SpiedFunction<typeof spiedObject.methodA>>(
spyOn(spiedObject, 'methodA'),
);
expectType<SpyInstance<typeof spiedObject.methodB>>(
expectType<SpiedFunction<typeof spiedObject.methodB>>(
spyOn(spiedObject, 'methodB'),
);
expectType<SpyInstance<typeof spiedObject.methodC>>(
expectType<SpiedFunction<typeof spiedObject.methodC>>(
spyOn(spiedObject, 'methodC'),
);

expectType<SpyInstance<() => boolean>>(spyOn(spiedObject, 'propertyB', 'get'));
expectType<SpyInstance<(value: boolean) => void>>(
expectType<SpiedGetter<typeof spiedObject.propertyB>>(
spyOn(spiedObject, 'propertyB', 'get'),
);
expectType<SpiedSetter<typeof spiedObject.propertyB>>(
spyOn(spiedObject, 'propertyB', 'set'),
);
expectError(spyOn(spiedObject, 'propertyB'));
expectError(spyOn(spiedObject, 'methodB', 'get'));
expectError(spyOn(spiedObject, 'methodB', 'set'));

expectType<SpyInstance<() => string>>(spyOn(spiedObject, 'propertyA', 'get'));
expectType<SpyInstance<(value: string) => void>>(
expectType<SpiedGetter<typeof spiedObject.propertyA>>(
spyOn(spiedObject, 'propertyA', 'get'),
);
expectType<SpiedSetter<typeof spiedObject.propertyA>>(
spyOn(spiedObject, 'propertyA', 'set'),
);
expectError(spyOn(spiedObject, 'propertyA'));
Expand All @@ -351,40 +363,38 @@ expectError(spyOn(true, 'methodA'));
expectError(spyOn(spiedObject));
expectError(spyOn());

expectType<SpyInstance<(arg: any) => boolean>>(
expectType<SpiedFunction<typeof Array.isArray>>(
spyOn(spiedArray as unknown as ArrayConstructor, 'isArray'),
);
expectError(spyOn(spiedArray, 'isArray'));

expectType<SpyInstance<() => string>>(
expectType<SpiedFunction<typeof spiedFunction.toString>>(
spyOn(spiedFunction as unknown as Function, 'toString'), // eslint-disable-line @typescript-eslint/ban-types
);
expectError(spyOn(spiedFunction, 'toString'));

expectType<SpyInstance<(value: string | number | Date) => Date>>(
spyOn(globalThis, 'Date'),
);
expectType<SpyInstance<() => number>>(spyOn(Date, 'now'));
expectType<SpiedClass<typeof Date>>(spyOn(globalThis, 'Date'));
expectType<SpiedFunction<typeof Date.now>>(spyOn(Date, 'now'));

// object with index signatures

expectType<SpyInstance<typeof indexSpiedObject.methodA>>(
expectType<SpiedFunction<typeof indexSpiedObject.methodA>>(
spyOn(indexSpiedObject, 'methodA'),
);
expectType<SpyInstance<typeof indexSpiedObject.methodB>>(
expectType<SpiedFunction<typeof indexSpiedObject.methodB>>(
spyOn(indexSpiedObject, 'methodB'),
);
expectType<SpyInstance<typeof indexSpiedObject.methodC>>(
expectType<SpiedFunction<typeof indexSpiedObject.methodC>>(
spyOn(indexSpiedObject, 'methodC'),
);
expectType<SpyInstance<typeof indexSpiedObject.methodE>>(
expectType<SpiedFunction<typeof indexSpiedObject.methodE>>(
spyOn(indexSpiedObject, 'methodE'),
);

expectType<SpyInstance<() => {a: string}>>(
expectType<SpiedGetter<typeof indexSpiedObject.propertyA>>(
spyOn(indexSpiedObject, 'propertyA', 'get'),
);
expectType<SpyInstance<(value: {a: string}) => void>>(
expectType<SpiedSetter<typeof indexSpiedObject.propertyA>>(
spyOn(indexSpiedObject, 'propertyA', 'set'),
);
expectError(spyOn(indexSpiedObject, 'propertyA'));
Expand Down Expand Up @@ -419,48 +429,48 @@ interface OptionalInterface {

const optionalSpiedObject = {} as OptionalInterface;

expectType<SpyInstance<(one: string) => SomeClass>>(
expectType<SpiedClass<NonNullable<typeof optionalSpiedObject.constructorA>>>(
spyOn(optionalSpiedObject, 'constructorA'),
);
expectType<SpyInstance<(one: string, two: boolean) => SomeClass>>(
expectType<SpiedClass<typeof optionalSpiedObject.constructorB>>(
spyOn(optionalSpiedObject, 'constructorB'),
);

expectError(spyOn(optionalSpiedObject, 'constructorA', 'get'));
expectError(spyOn(optionalSpiedObject, 'constructorA', 'set'));

expectType<SpyInstance<(a: boolean) => void>>(
expectType<SpiedFunction<NonNullable<typeof optionalSpiedObject.methodA>>>(
spyOn(optionalSpiedObject, 'methodA'),
);
expectType<SpyInstance<(b: string) => boolean>>(
expectType<SpiedFunction<typeof optionalSpiedObject.methodB>>(
spyOn(optionalSpiedObject, 'methodB'),
);

expectError(spyOn(optionalSpiedObject, 'methodA', 'get'));
expectError(spyOn(optionalSpiedObject, 'methodA', 'set'));

expectType<SpyInstance<() => number>>(
expectType<SpiedGetter<NonNullable<typeof optionalSpiedObject.propertyA>>>(
spyOn(optionalSpiedObject, 'propertyA', 'get'),
);
expectType<SpyInstance<(arg: number) => void>>(
expectType<SpiedSetter<NonNullable<typeof optionalSpiedObject.propertyA>>>(
spyOn(optionalSpiedObject, 'propertyA', 'set'),
);
expectType<SpyInstance<() => number>>(
expectType<SpiedGetter<NonNullable<typeof optionalSpiedObject.propertyB>>>(
spyOn(optionalSpiedObject, 'propertyB', 'get'),
);
expectType<SpyInstance<(arg: number) => void>>(
expectType<SpiedSetter<NonNullable<typeof optionalSpiedObject.propertyB>>>(
spyOn(optionalSpiedObject, 'propertyB', 'set'),
);
expectType<SpyInstance<() => number | undefined>>(
expectType<SpiedGetter<typeof optionalSpiedObject.propertyC>>(
spyOn(optionalSpiedObject, 'propertyC', 'get'),
);
expectType<SpyInstance<(arg: number | undefined) => void>>(
expectType<SpiedSetter<typeof optionalSpiedObject.propertyC>>(
spyOn(optionalSpiedObject, 'propertyC', 'set'),
);
expectType<SpyInstance<() => string>>(
expectType<SpiedGetter<typeof optionalSpiedObject.propertyD>>(
spyOn(optionalSpiedObject, 'propertyD', 'get'),
);
expectType<SpyInstance<(arg: string) => void>>(
expectType<SpiedSetter<typeof optionalSpiedObject.propertyD>>(
spyOn(optionalSpiedObject, 'propertyD', 'set'),
);

Expand Down
60 changes: 34 additions & 26 deletions packages/jest-mock/src/index.ts
Expand Up @@ -100,6 +100,29 @@ export type MockedShallow<T> = T extends ClassLike
: T;

export type UnknownFunction = (...args: Array<unknown>) => unknown;
export type UnknownClass = {new (...args: Array<unknown>): unknown};

export type SpiedClass<T extends ClassLike = UnknownClass> = MockInstance<
(...args: ConstructorParameters<T>) => InstanceType<T>
>;

export type SpiedFunction<T extends FunctionLike = UnknownFunction> =
MockInstance<(...args: Parameters<T>) => ReturnType<T>>;

export type SpiedGetter<T> = MockInstance<() => T>;

export type SpiedSetter<T> = MockInstance<(arg: T) => void>;

export type Spied<T extends ClassLike | FunctionLike> = T extends ClassLike
? MockInstance<(...args: ConstructorParameters<T>) => InstanceType<T>>
: T extends FunctionLike
? MockInstance<(...args: Parameters<T>) => ReturnType<T>>
: never;

// TODO in Jest 30 remove `SpyInstance` in favour of `Spied`
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SpyInstance<T extends FunctionLike = UnknownFunction>
extends MockInstance<T> {}

/**
* All what the internal typings need is to be sure that we have any-function.
Expand Down Expand Up @@ -149,10 +172,6 @@ export interface MockInstance<T extends FunctionLike = UnknownFunction> {
mockRejectedValueOnce(value: RejectType<T>): this;
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface SpyInstance<T extends FunctionLike = UnknownFunction>
extends MockInstance<T> {}

type MockFunctionResultIncomplete = {
type: 'incomplete';
/**
Expand Down Expand Up @@ -1080,10 +1099,9 @@ export class ModuleMocker {
}

isMockFunction<T extends FunctionLike = UnknownFunction>(
fn: SpyInstance<T>,
): fn is SpyInstance<T>;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint
isMockFunction<P extends Array<unknown>, R extends unknown>(
fn: MockInstance<T>,
): fn is MockInstance<T>;
isMockFunction<P extends Array<unknown>, R>(
fn: (...args: P) => R,
): fn is Mock<(...args: P) => R>;
isMockFunction(fn: unknown): fn is Mock<UnknownFunction>;
Expand All @@ -1107,35 +1125,25 @@ export class ModuleMocker {
T extends object,
K extends PropertyLikeKeys<T>,
V extends Required<T>[K],
>(object: T, methodKey: K, accessType: 'get'): SpyInstance<() => V>;

spyOn<
T extends object,
K extends PropertyLikeKeys<T>,
V extends Required<T>[K],
>(object: T, methodKey: K, accessType: 'set'): SpyInstance<(arg: V) => void>;

spyOn<
T extends object,
K extends ConstructorLikeKeys<T>,
V extends Required<T>[K],
A extends 'get' | 'set',
>(
object: T,
methodKey: K,
): V extends ClassLike
? SpyInstance<(...args: ConstructorParameters<V>) => InstanceType<V>>
accessType: A,
): A extends 'get'
? SpiedGetter<V>
: A extends 'set'
? SpiedSetter<V>
: never;

spyOn<
T extends object,
K extends MethodLikeKeys<T>,
K extends ConstructorLikeKeys<T> | MethodLikeKeys<T>,
V extends Required<T>[K],
>(
object: T,
methodKey: K,
): V extends FunctionLike
? SpyInstance<(...args: Parameters<V>) => ReturnType<V>>
: never;
): V extends ClassLike | FunctionLike ? Spied<V> : never;

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
spyOn<T extends object, K extends PropertyLikeKeys<T>>(
Expand Down