strong-mock
Version:
Type safe mocking library for TypeScript
585 lines (564 loc) • 18.8 kB
TypeScript
declare const MATCHER_SYMBOL: unique symbol;
type MatcherDiffer = (actual: any) => {
actual: any;
expected: any;
};
type MatcherOptions = {
/**
* Will be called when printing the diff between an expectation and the
* (mismatching) received arguments.
*
* With this function you can pretty print the `actual` and `expected` values
* according to your matcher's logic.
*
* @param actual The actual value received by this matcher, same as the one
* in `matches`.
*
* @example
* const neverMatcher = It.matches(() => false, {
* getDiff: (actual) => ({ actual, expected: 'never' })
* });
*
* when(() => fn(neverMatcher)).thenReturn(42);
*
* fn(42);
* // - Expected
* // + Received
* //
* // - 'never'
* // + 42
*/
getDiff: MatcherDiffer;
/**
* Will be called when printing arguments for an unexpected or unmet expectation.
*
* @example
* const neverMatcher = It.matches(() => false, {
* toString: () => 'never'
* });
* when(() => fn(neverMatcher)).thenReturn(42);
*
* fn(42);
* // Unmet expectations:
* // when(() => fn(never)).thenReturn(42)
*/
toString: () => string;
};
/**
* You MUST use {@link It.matches} to create this branded type.
*/
interface Matcher extends MatcherOptions {
[MATCHER_SYMBOL]: boolean;
/**
* Will be called with the received value and should return whether it matches
* the expectation.
*/
matches: (actual: any) => boolean;
}
/**
* This takes the shape of T to satisfy call sites, but strong-mock will only
* care about the matcher type.
*/
type TypeMatcher<T> = T & Matcher;
/**
* Create a custom matcher.
*
* @param predicate Will receive the actual value and return whether it matches the expectation.
* @param options
* @param options.toString An optional function that should return a string that will be
* used when the matcher needs to be printed in an error message. By default,
* it stringifies `predicate`.
* @param options.getDiff An optional function that will be called when printing the
* diff for a failed expectation. It will only be called if there's a mismatch
* between the expected and received values i.e. `predicate(actual)` fails.
* By default, the `toString` method will be used to format the expected value,
* while the received value will be returned as-is.
*
* @example
* // Create a matcher for positive numbers.
* const fn = mock<(x: number) => number>();
* when(() => fn(It.matches(x => x >= 0))).thenReturn(42);
*
* fn(2) === 42
* fn(-1) // throws
*/
declare const matches: <T>(predicate: (actual: T) => boolean, options?: Partial<MatcherOptions>) => TypeMatcher<T>;
type ConcreteMatcher = <T>(expected: T) => Matcher;
declare enum UnexpectedProperty {
/**
* Throw an error immediately.
*
* @example
* // Will throw "Didn't expect foo to be accessed".
* const { foo } = service;
*
* // Will throw "Didn't expect foo to be accessed",
* // without printing the arguments.
* foo(42);
*/
THROW = 0,
/**
* Return a function that will throw if called. This can be useful if your
* code destructures a function but never calls it.
*
* It will also improve error messages for unexpected calls because arguments
* will be captured instead of throwing immediately on the property access.
*
* The function will be returned even if the property is not supposed to be a
* function. This could cause weird behavior at runtime, when your code expects
* e.g. a number and gets a function instead.
*
* @example
* // This will NOT throw.
* const { foo } = service;
*
* // This will NOT throw, and might produce unexpected results.
* foo > 0
*
* // Will throw "Didn't expect foo(42) to be called".
* foo(42);
*/
CALL_THROW = 1
}
interface MockOptions {
/**
* The name of the mock that will be used in error messages. Defaults to `mock`.
*
* This can be useful when you want to easily identify the mock from the test
* output, especially if you have multiple mocks in the same test.
*
* @example
* const service = mock<Service>({ name: 'Service' });
* service.foo() // "Didn't expect Service.foo() to be called"
*/
name?: string;
/**
* Controls what should be returned for a property with no expectations.
*
* A property with no expectations is a property that has no `when`
* expectations set on it. It can also be a property that ran out of `when`
* expectations.
*
* The default is to return a function that will throw when called.
*
* @example
* const foo = mock<{ bar: () => number }>();
* foo.bar() // unexpected property access
*
* @example
* const foo = mock<{ bar: () => number }>();
* when(() => foo.bar()).thenReturn(42);
* foo.bar() === 42
* foo.bar() // unexpected property access
*/
unexpectedProperty?: UnexpectedProperty;
/**
* If `true`, the number of received arguments in a function/method call has to
* match the number of arguments set in the expectation.
*
* If `false`, extra parameters are considered optional and checked by the
* TypeScript compiler instead.
*
* You may want to set this to `true` if you're not using TypeScript,
* or if you want to be extra strict.
*
* @example
* const fn = mock<(value?: number) => number>({ exactParams: true });
* when(() => fn()).thenReturn(42);
*
* fn(100) // throws with exactParams, returns 42 without
*/
exactParams?: boolean;
/**
* The matcher that will be used when one isn't specified explicitly.
*
* The most common use case is replacing the default {@link It.deepEquals}
* matcher with {@link It.is}, but you can also use {@link It.matches} to
* create a custom matcher.
*
* @param expected The concrete expected value received from the
* {@link when} expectation.
*
* @example
* const fn = mock<(value: number[]) => string>({
* concreteMatcher: It.is
* });
*
* const expected = [1, 2, 3];
* when(() => fn(expected).thenReturn('matched');
*
* fn([1, 2, 3]); // throws because different array instance
* fn(expected); // matched
*/
concreteMatcher?: ConcreteMatcher;
}
type Mock<T> = T;
/**
* Create a type safe mock.
*
* @see {@link when} Set expectations on the mock using `when`.
*
* @param options Configure the options for this specific mock, overriding any
* defaults that were set with {@link setDefaults}.
* @param options.name The name of the mock that appears in error messages.
* @param options.unexpectedProperty Controls what happens when an unexpected
* property is accessed.
* @param options.concreteMatcher The matcher that will be used when one isn't
* specified explicitly.
* @param options.exactParams Controls whether the number of received arguments
* has to match the expectation.
*
* @example
* const fn = mock<() => number>();
*
* when(() => fn()).thenReturn(23);
*
* fn() === 23;
*/
declare const mock: <T>({ name, unexpectedProperty, concreteMatcher, exactParams, }?: MockOptions) => Mock<T>;
type Property = string | symbol;
interface InvocationCount {
/**
* Expect a call to be made at least `min` times and at most `max` times.
*/
between: (min: number, max: number) => void;
/**
* Expect a call to be made exactly `exact` times.
*
* Shortcut for `between(exact, exact)`.
*/
times: (exact: number) => void;
/**
* Expect a call to be made any number of times, including never.
*
* Shortcut for `between(0, Infinity)`.
*/
anyTimes: () => void;
/**
* Expect a call to be made at least `min` times.
*
* Shortcut for `between(min, Infinity)`.
*/
atLeast: (min: number) => void;
/**
* Expect a call to be made at most `max` times.
*
* Shortcut for `between(0, max)`.
*/
atMost: (max: number) => void;
/**
* Expect a call to be made exactly once.
*
* Shortcut for `times(1)`.
*/
once: () => void;
/**
* Expect a call to be made exactly twice.
*
* Shortcut for `times(2)`.
*/
twice: () => void;
}
type PromiseStub<R, P> = {
/**
* Set the return value for the current call.
*
* @param value This needs to be of the same type as the value returned
* by the call inside `when`.
*
* @example
* when(() => fn()).thenReturn(Promise.resolve(23));
*
* @example
* when(() => fn()).thenReturn(Promise.reject({ foo: 'bar' });
*/
thenReturn: (value: P) => InvocationCount;
/**
* Set the return value for the current call.
*
* @param promiseValue This needs to be of the same type as the value inside
* the promise returned by the `when` callback.
*
* @example
* when(() => fn()).thenResolve('foo');
*/
thenResolve: (promiseValue: R) => InvocationCount;
/**
* Make the current call reject with the given error.
*
* @param error An `Error` instance. You can pass just a message, and
* it will be wrapped in an `Error` instance. If you want to reject with
* a non error then use the {@link thenReturn} method.
*
* @example
* when(() => fn()).thenReject(new Error('oops'));
*/
thenReject: ((error: Error) => InvocationCount) & ((message: string) => InvocationCount) & (() => InvocationCount);
};
type NonPromiseStub<R> = {
/**
* Set the return value for the current call.
*
* @param returnValue This needs to be of the same type as the value returned
* by the `when` callback.
*/
thenReturn: (returnValue: R) => InvocationCount;
/**
* Make the current call throw the given error.
*
* @param error The error instance. If you want to throw a simple `Error`
* you can pass just the message.
*/
thenThrow: ((error: Error) => InvocationCount) & ((message: string) => InvocationCount) & (() => InvocationCount);
};
interface When {
<R>(expectation: () => Promise<R>): PromiseStub<R, Promise<R>>;
<R>(expectation: () => R): NonPromiseStub<R>;
}
/**
* Set an expectation on a mock.
*
* The expectation must be finished by setting a return value, even if the value
* is `undefined`.
*
* If a call happens that was not expected, then the mock will throw an error.
* By default, the call is expected only once. Use the invocation count helpers
* to expect a call multiple times.
*
* @param expectation A callback to set the expectation on your mock. The
* callback must return the value from the mock to properly infer types.
*
* @see {@link It.deepEquals} All values are wrapped in the default matcher.
* @see {@link It} for more matchers.
*
* @example
* const fn = mock<() => void>();
* when(() => fn()).thenReturn(undefined);
*
* @example
* const fn = mock<() => number>();
* when(() => fn()).thenReturn(42).atMost(3);
*
* @example
* const fn = mock<(x: number) => Promise<number>();
* when(() => fn(23)).thenResolve(42);
*/
declare const when: When;
/**
* Remove any remaining expectations on the given mock.
*
* @example
* const fn = mock<() => number>();
*
* when(() => fn()).thenReturn(23);
*
* reset(fn);
*
* fn(); // throws
*/
declare const reset: (mock: Mock<any>) => void;
/**
* Reset all existing mocks.
*
* @see reset
*/
declare const resetAll: () => void;
/**
* Verify that all expectations on the given mock have been met.
*
* @throws Will throw if there are remaining expectations that were set
* using `when` and that weren't met.
*
* @throws Will throw if any unexpected calls happened. Normally those
* calls throw on their own, but the error might be caught by the code
* being tested.
*
* @example
* const fn = mock<() => number>();
*
* when(() => fn()).thenReturn(23);
*
* verify(fn); // throws
*/
declare const verify: <T>(mock: Mock<T>) => void;
/**
* Verify all existing mocks.
*
* @see verify
*/
declare const verifyAll: () => void;
/**
* Override strong-mock's defaults.
*
* @param newDefaults These will be applied to the library defaults. Multiple
* calls don't stack e.g. calling this with `{}` will clear any previously
* applied defaults.
*/
declare const setDefaults: (newDefaults: MockOptions) => void;
/**
* Compare values using deep equality.
*
* This is the default matcher that's automatically used when no other matcher
* is specified. You can change it with the `concreteMatcher` option when creating
* a {@link mock}, or set a new default with {@link setDefaults}.
*
* @param expected Supports nested matchers for objects, arrays, and Maps.
* @param strict By default, this matcher will treat a missing key in an object
* and a key with the value `undefined` as not equal. It will also consider
* non `Object` instances with different constructors as not equal. Setting
* this to `false` will consider the objects in both cases as equal.
*
* @see {@link It.containsObject} or {@link It.isArray} for partially matching
* objects or arrays respectively.
* @see {@link It.is} for strict equality.
*
* @example
* const fn = mock<(x: { foo: { bar: number } }) => number>();
*
* // deepEquals is the default matcher.
* when(() => fn({ foo: { bar: 42 } })).thenReturn(1);
*
* // With nested matchers:
* when(() => fn({ foo: { bar: It.isNumber() } })).thenReturn(2);
*/
declare const deepEquals: <T>(expected: T, { strict, }?: {
strict?: boolean;
}) => TypeMatcher<T>;
/**
* Compare values using `Object.is`.
*
* @link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is
*
* @see It.deepEquals A matcher that uses deep equality.
*/
declare const is: <T = unknown>(expected: T) => TypeMatcher<T>;
/**
* Match any value, including `undefined` and `null`.
*
* @example
* const fn = mock<(x: number, y: string) => number>();
* when(() => fn(It.isAny(), It.isAny())).thenReturn(1);
*
* fn(23, 'foobar') === 1
*/
declare const isAny: () => TypeMatcher<any>;
/**
* Match an array.
*
* Supports nested matchers.
*
* @param containing If given, the matched array has to contain ALL of these
* elements in ANY order. Use {@link It.deepEquals} or {@link It.matches} if
* you want more control.
*
* @example
* const fn = mock<(arr: number[]) => number>();
* when(() => fn(It.isArray())).thenReturn(1);
* when(() => fn(It.isArray([2, 3]))).thenReturn(2);
*
* fn({ length: 1, 0: 42 }) // throws
* fn([]) === 1
* fn([3, 2, 1]) === 2
*
* @example
* It.isArray([It.isString({ containing: 'foobar' })])
*/
declare const isArray: <T extends unknown[]>(containing?: T) => TypeMatcher<T>;
/**
* Match any number.
*
* @example
* const fn = mock<(x: number) => number>();
* when(() => fn(It.isNumber())).returns(42);
*
* fn(20.5) === 42
* fn(NaN) // throws
*/
declare const isNumber: () => TypeMatcher<number>;
type ObjectType$1 = Record<Property, unknown>;
/**
* Matches any plain object e.g. object literals or objects created with `Object.create()`.
*
* Classes, arrays, maps, sets etc. are not considered plain objects.
* You can use {@link containsObject} or {@link matches} to match those.
*
* @example
* const fn = mock<({ foo: string }) => number>();
* when(() => fn(It.isPlainObject())).thenReturn(42);
*
* fn({ foo: 'bar' }) // returns 42
*/
declare const isPlainObject: <T extends ObjectType$1>() => TypeMatcher<T>;
type ObjectType = Record<Property, unknown>;
type NonEmptyObject<T extends ObjectType> = keyof T extends never ? never : T;
type DeepPartial<T> = T extends ObjectType ? {
[K in keyof T]?: DeepPartial<T[K]>;
} : T;
/**
* Check if an object recursively contains the expected properties,
* i.e. the expected object is a subset of the received object.
*
* @param partial A subset of the expected object that will be recursively matched.
* Supports nested matchers.
* Concrete values will be compared with {@link deepEquals}.
*
* @see {@link isPlainObject} if you want to match any plain object.
*
* @example
* const fn = mock<(pos: { x: number, y: number }) => number>();
* when(() => fn(It.containsObject({ x: 23 }))).returns(42);
*
* fn({ x: 23, y: 200 }) // returns 42
*
* @example
* It.containsObject({ foo: It.isString() })
*/
declare const containsObject: <T, K extends DeepPartial<T>>(partial: K extends ObjectType ? NonEmptyObject<K> : never) => TypeMatcher<T>;
/**
* Match any string.
*
* @param matching An optional string or RegExp to match the string against.
* If it's a string, a case-sensitive search will be performed.
*
* @example
* const fn = mock<(x: string, y: string) => number>();
* when(() => fn(It.isString(), It.isString('bar'))).returns(42);
*
* fn('foo', 'baz') // throws
* fn('foo', 'bar') === 42
*/
declare const isString: (matching?: string | RegExp) => TypeMatcher<string>;
/**
* Matches anything and stores the received value.
*
* This should not be needed for most cases, but can be useful if you need
* access to a complex argument outside the expectation e.g. to test a
* callback.
*
* @param name If given, this name will be printed in error messages.
*
* @example
* const fn = mock<(cb: (value: number) => number) => void>();
* const matcher = It.willCapture();
* when(() => fn(matcher)).thenReturn();
*
* fn(x => x + 1);
* matcher.value?.(3) === 4
*/
declare const willCapture: <T = unknown>(name?: string) => TypeMatcher<T> & {
value: T | undefined;
};
declare const it_containsObject: typeof containsObject;
declare const it_deepEquals: typeof deepEquals;
declare const it_is: typeof is;
declare const it_isAny: typeof isAny;
declare const it_isArray: typeof isArray;
declare const it_isNumber: typeof isNumber;
declare const it_isPlainObject: typeof isPlainObject;
declare const it_isString: typeof isString;
declare const it_matches: typeof matches;
declare const it_willCapture: typeof willCapture;
declare namespace it {
export { it_containsObject as containsObject, it_deepEquals as deepEquals, it_is as is, it_isAny as isAny, it_isArray as isArray, it_isNumber as isNumber, it_isPlainObject as isPlainObject, it_isString as isString, it_matches as matches, it_willCapture as willCapture };
}
export { it as It, type Matcher, type MatcherOptions, type MockOptions, UnexpectedProperty, mock, reset, resetAll, setDefaults, verify, verifyAll, when };