Skip to content

Commit

Permalink
[node] Add mocking
Browse files Browse the repository at this point in the history
  • Loading branch information
Semigradsky committed Feb 11, 2023
1 parent 0eee833 commit e044e9e
Show file tree
Hide file tree
Showing 4 changed files with 1,055 additions and 2 deletions.
239 changes: 238 additions & 1 deletion types/node/test.d.ts
Expand Up @@ -342,6 +342,10 @@ declare module 'node:test' {
* @returns A {@link Promise} resolved with `undefined` once the test completes.
*/
test: typeof test;
/**
* Each test provides its own MockTracker instance.
*/
readonly mock: MockTracker;
}

interface TestOptions {
Expand Down Expand Up @@ -451,5 +455,238 @@ declare module 'node:test' {
timeout?: number | undefined;
}

export { test as default, run, test, describe, it, before, after, beforeEach, afterEach };
interface MockFunctionOptions {
/**
* The number of times that the mock will use the behavior of `implementation`.
* Once the mock function has been called `times` times,
* it will automatically restore the behavior of `original`.
* This value must be an integer greater than zero.
* @default Infinity
*/
times?: number | undefined;
}

interface MockMethodOptions extends MockFunctionOptions {
/**
* If `true`, `object[methodName]` is treated as a getter.
* This option cannot be used with the `setter` option.
*/
getter?: boolean | undefined;

/**
* If `true`, `object[methodName]` is treated as a setter.
* This option cannot be used with the `getter` option.
*/
setter?: boolean | undefined;
}

type Mock<F extends Function> = F & {
mock: MockFunctionContext<F>;
};

type NoOpFunction = (...args: any[]) => undefined;

type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

interface MockTracker {
/**
* This function is used to create a mock function.
* @param original An optional function to create a mock on.
* @param implementation An optional function used as the mock implementation for `original`.
* This is useful for creating mocks that exhibit one behavior for a specified number of calls and then restore the behavior of `original`.
* @param options Optional configuration options for the mock function.
*/
fn<F extends Function = NoOpFunction>(original?: F, options?: MockFunctionOptions): Mock<F>;
fn<F extends Function = NoOpFunction, Implementation extends Function = F>(original?: F, implementation?: Implementation, options?: MockFunctionOptions): Mock<F | Implementation>;

/**
* This function is used to create a mock on an existing object method.
* @param object The object whose method is being mocked.
* @param methodName The identifier of the method on `object` to mock. If `object[methodName]` is not a function, an error is thrown.
* @param implementation An optional function used as the mock implementation for `object[methodName]`.
* @param options Optional configuration options for the mock method.
*/
method<
MockedObject extends object,
MethodName extends FunctionPropertyNames<MockedObject>,
>(
object: MockedObject,
methodName: MethodName,
options?: MockFunctionOptions,
): MockedObject[MethodName] extends Function
? Mock<MockedObject[MethodName]>
: never;
method<
MockedObject extends object,
MethodName extends FunctionPropertyNames<MockedObject>,
Implementation extends Function,
>(
object: MockedObject,
methodName: MethodName,
implementation: Implementation,
options?: MockFunctionOptions,
): MockedObject[MethodName] extends Function
? Mock<MockedObject[MethodName] | Implementation>
: never;
method<MockedObject extends object>(
object: MockedObject,
methodName: keyof MockedObject,
options: MockMethodOptions,
): Mock<Function>;
method<MockedObject extends object>(
object: MockedObject,
methodName: keyof MockedObject,
implementation: Function,
options: MockMethodOptions,
): Mock<Function>;

/**
* This function is syntax sugar for {@link MockTracker.method} with `options.getter` set to `true`.
*/
getter<
MockedObject extends object,
MethodName extends keyof MockedObject,
>(
object: MockedObject,
methodName: MethodName,
options?: MockFunctionOptions,
): Mock<() => MockedObject[MethodName]>;
getter<
MockedObject extends object,
MethodName extends keyof MockedObject,
Implementation extends Function,
>(
object: MockedObject,
methodName: MethodName,
implementation?: Implementation,
options?: MockFunctionOptions,
): Mock<(() => MockedObject[MethodName]) | Implementation>;

/**
* This function is syntax sugar for {@link MockTracker.method} with `options.setter` set to `true`.
*/
setter<
MockedObject extends object,
MethodName extends keyof MockedObject,
>(
object: MockedObject,
methodName: MethodName,
options?: MockFunctionOptions,
): Mock<(value: MockedObject[MethodName]) => void>;
setter<
MockedObject extends object,
MethodName extends keyof MockedObject,
Implementation extends Function,
>(
object: MockedObject,
methodName: MethodName,
implementation?: Implementation,
options?: MockFunctionOptions,
): Mock<((value: MockedObject[MethodName]) => void) | Implementation>;

/**
* This function restores the default behavior of all mocks that were previously created by this `MockTracker`
* and disassociates the mocks from the `MockTracker` instance. Once disassociated, the mocks can still be used,
* but the `MockTracker` instance can no longer be used to reset their behavior or otherwise interact with them.
*
* After each test completes, this function is called on the test context's `MockTracker`.
* If the global `MockTracker` is used extensively, calling this function manually is recommended.
*/
reset(): void;

/**
* This function restores the default behavior of all mocks that were previously created by this `MockTracker`.
* Unlike `mock.reset()`, `mock.restoreAll()` does not disassociate the mocks from the `MockTracker` instance.
*/
restoreAll(): void;
}

const mock: MockTracker;

interface MockFunctionCall<
F extends Function,
ReturnType = F extends (...args: any) => infer T
? T
: F extends abstract new (...args: any) => infer T
? T
: unknown,
Args = F extends (...args: infer Y) => any
? Y
: F extends abstract new (...args: infer Y) => any
? Y
: unknown[],
> {
/**
* An array of the arguments passed to the mock function.
*/
arguments: Args;
/**
* If the mocked function threw then this property contains the thrown value.
*/
error: unknown | undefined;
/**
* The value returned by the mocked function.
*
* If the mocked function threw, it will be `undefined`.
*/
result: ReturnType | undefined;
/**
* An `Error` object whose stack can be used to determine the callsite of the mocked function invocation.
*/
stack: Error;
/**
* If the mocked function is a constructor, this field contains the class being constructed.
* Otherwise this will be `undefined`.
*/
target: F extends abstract new (...args: any) => any ? F : undefined;
/**
* The mocked function's `this` value.
*/
this: unknown;
}

interface MockFunctionContext<F extends Function> {
/**
* A getter that returns a copy of the internal array used to track calls to the mock.
*/
readonly calls: Array<MockFunctionCall<F>>;

/**
* This function returns the number of times that this mock has been invoked.
* This function is more efficient than checking `ctx.calls.length`
* because `ctx.calls` is a getter that creates a copy of the internal call tracking array.
*/
callCount(): number;

/**
* This function is used to change the behavior of an existing mock.
* @param implementation The function to be used as the mock's new implementation.
*/
mockImplementation(implementation: Function): void;

/**
* This function is used to change the behavior of an existing mock for a single invocation.
* Once invocation `onCall` has occurred, the mock will revert to whatever behavior
* it would have used had `mockImplementationOnce()` not been called.
* @param implementation The function to be used as the mock's implementation for the invocation number specified by `onCall`.
* @param onCall The invocation number that will use `implementation`.
* If the specified invocation has already occurred then an exception is thrown.
*/
mockImplementationOnce(implementation: Function, onCall?: number): void;

/**
* Resets the call history of the mock function.
*/
resetCalls(): void;

/**
* Resets the implementation of the mock function to its original behavior.
* The mock can still be used after calling this function.
*/
restore(): void;
}

export { test as default, run, test, describe, it, before, after, beforeEach, afterEach, mock };
}

0 comments on commit e044e9e

Please sign in to comment.