UNPKG

strong-mock

Version:

Type safe mocking library for TypeScript

585 lines (564 loc) 18.8 kB
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 };