From 7213d527ec552907daec32f16143f53b73d50a3c Mon Sep 17 00:00:00 2001 From: Kyle June Date: Tue, 29 Mar 2022 07:51:36 -0500 Subject: [PATCH] feat(testing): add mocking utilities (#2048) --- testing/README.md | 211 ++ testing/_test_utils.ts | 18 + testing/mock.ts | 831 ++++++++ testing/mock_examples/internals_injection.ts | 9 + .../mock_examples/internals_injection_test.ts | 23 + testing/mock_examples/parameter_injection.ts | 10 + .../mock_examples/parameter_injection_test.ts | 18 + testing/mock_examples/random.ts | 9 + testing/mock_examples/random_test.ts | 29 + testing/mock_test.ts | 1783 +++++++++++++++++ 10 files changed, 2941 insertions(+) create mode 100755 testing/_test_utils.ts create mode 100644 testing/mock.ts create mode 100644 testing/mock_examples/internals_injection.ts create mode 100644 testing/mock_examples/internals_injection_test.ts create mode 100644 testing/mock_examples/parameter_injection.ts create mode 100644 testing/mock_examples/parameter_injection_test.ts create mode 100644 testing/mock_examples/random.ts create mode 100644 testing/mock_examples/random_test.ts create mode 100644 testing/mock_test.ts diff --git a/testing/README.md b/testing/README.md index 932eb15c1bf6..7538c1215398 100644 --- a/testing/README.md +++ b/testing/README.md @@ -277,3 +277,214 @@ with the `BenchmarkRunOptions.silent` flag. Clears all registered benchmarks, so calling `runBenchmarks()` after it wont run them. Filtering can be applied by setting `BenchmarkRunOptions.only` and/or `BenchmarkRunOptions.skip` to regular expressions matching benchmark names. + +## Mocking + +Test spies are function stand-ins that are used to assert if a function's +internal behavior matches expectations. Test spies on methods keep the original +behavior but allow you to test how the method is called and what it returns. +Test stubs are an extension of test spies that also replaces the original +methods behavior. + +### Spying + +Say we have two functions, `square` and `multiply`, if we want to assert that +the `multiply` function is called during execution of the `square` function we +need a way to spy on the `multiple` function. There are a few ways to achieve +this with Spies, one is to have the `square` function take the `multiply` +multiply as a parameter. + +```ts +// https://deno.land/std@$STD_VERSION/testing/mock_examples/parameter_injection.ts +export function multiply(a: number, b: number): number { + return a * b; +} + +export function square( + multiplyFn: (a: number, b: number) => number, + value: number, +): number { + return multiplyFn(value, value); +} +``` + +This way, we can call `square(multiply, value)` in the application code or wrap +a spy function around the `multiply` function and call +`square(multiplySpy, value)` in the testing code. + +```ts +// https://deno.land/std@$STD_VERSION/testing/mock_examples/parameter_injection_test.ts +import { + assertSpyCall, + assertSpyCalls, + spy, +} from "https://deno.land/std@$STD_VERSION/testing/mock.ts"; +import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { + multiply, + square, +} from "https://deno.land/std@$STD_VERSION/testing/mock_examples/parameter_injection.ts"; + +Deno.test("square calls multiply and returns results", () => { + const multiplySpy = spy(multiply); + + assertEquals(square(multiplySpy, 5), 25); + + // asserts that multiplySpy was called at least once and details about the first call. + assertSpyCall(multiplySpy, 0, { + args: [5, 5], + returned: 25, + }); + + // asserts that multiplySpy was only called once. + assertSpyCalls(multiplySpy, 1); +}); +``` + +If you prefer not adding additional parameters for testing purposes only, you +can use spy to wrap a method on an object instead. In the following example, the +exported `_internals` object has the `multiply` function we want to call as a +method and the `square` function calls `_internals.multiply` instead of +`multiply`. + +```ts +// https://deno.land/std@$STD_VERSION/testing/mock_examples/internals_injection.ts +export function multiply(a: number, b: number): number { + return a * b; +} + +export function square(value: number): number { + return _internals.multiply(value, value); +} + +export const _internals = { multiply }; +``` + +This way, we can call `square(value)` in both the application code and testing +code. Then spy on the `multiply` method on the `_internals` object in the +testing code to be able to spy on how the `square` function calls the `multiply` +function. + +```ts +// https://deno.land/std@$STD_VERSION/testing/mock_examples/internals_injection_test.ts +import { + assertSpyCall, + assertSpyCalls, + spy, +} from "https://deno.land/std@$STD_VERSION/testing/mock.ts"; +import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { + _internals, + square, +} from "https://deno.land/std@$STD_VERSION/testing/mock_examples/internals_injection.ts"; + +Deno.test("square calls multiply and returns results", () => { + const multiplySpy = spy(_internals, "multiply"); + + try { + assertEquals(square(5), 25); + } finally { + // unwraps the multiply method on the _internals object + multiplySpy.restore(); + } + + // asserts that multiplySpy was called at least once and details about the first call. + assertSpyCall(multiplySpy, 0, { + args: [5, 5], + returned: 25, + }); + + // asserts that multiplySpy was only called once. + assertSpyCalls(multiplySpy, 1); +}); +``` + +One difference you may have noticed between these two examples is that in the +second we call the `restore` method on `multiplySpy` function. That is needed to +remove the spy wrapper from the `_internals` object's `multiply` method. The +`restore` method is called in a finally block to ensure that it is restored +whether or not the assertion in the try block is successful. The `restore` +method didn't need to be called in the first example because the `multiply` +function was not modified in any way like the `_internals` object was in the +second example. + +### Stubbing + +Say we have two functions, `randomMultiple` and `randomInt`, if we want to +assert that `randomInt` is called during execution of `randomMultiple` we need a +way to spy on the `randomInt` function. That could be done with either either of +the spying techniques previously mentioned. To be able to verify that the +`randomMultiple` function returns the value we expect it to for what `randomInt` +returns, the easiest way would be to replace the `randomInt` function's behavior +with more predictable behavior. + +You could use the first spying technique to do that but that would require +adding a `randomInt` parameter to the `randomMultiple` function. + +You could also use the second spying technique to do that, but your assertions +would not be as predictable due to the `randomInt` function returning random +values. + +Say we want to verify it returns correct values for both negative and positive +random integers. We could easily do that with stubbing. The below example is +similar to the second spying technique example but instead of passing the call +through to the original `randomInt` function, we are going to replace +`randomInt` with a function that returns pre-defined values. + +```ts +// https://deno.land/std@$STD_VERSION/testing/mock_examples/random.ts +export function randomInt(lowerBound: number, upperBound: number): number { + return lowerBound + Math.floor(Math.random() * (upperBound - lowerBound)); +} + +export function randomMultiple(value: number): number { + return value * _internals.randomInt(-10, 10); +} + +export const _internals = { randomInt }; +``` + +The mock module includes some helper functions to make creating common stubs +easy. The `returnsNext` function takes an array of values we want it to return +on consecutive calls. + +```ts +// https://deno.land/std@$STD_VERSION/testing/mock_examples/random_test.ts +import { + assertSpyCall, + assertSpyCalls, + returnsNext, + stub, +} from "https://deno.land/std@$STD_VERSION/testing/mock.ts"; +import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { + _internals, + randomMultiple, +} from "https://deno.land/std@$STD_VERSION/testing/mock_examples/random.ts"; + +Deno.test("randomMultiple uses randomInt to generate random multiples between -10 and 10 times the value", () => { + const randomIntStub = stub(_internals, "randomInt", returnsNext([-3, 3])); + + try { + assertEquals(randomMultiple(5), -15); + assertEquals(randomMultiple(5), 15); + } finally { + // unwraps the randomInt method on the _internals object + randomIntStub.restore(); + } + + // asserts that randomIntStub was called at least once and details about the first call. + assertSpyCall(randomIntStub, 0, { + args: [-10, 10], + returned: -3, + }); + // asserts that randomIntStub was called at least twice and details about the second call. + assertSpyCall(randomIntStub, 1, { + args: [-10, 10], + returned: 3, + }); + + // asserts that randomIntStub was only called twice. + assertSpyCalls(randomIntStub, 2); +}); +``` diff --git a/testing/_test_utils.ts b/testing/_test_utils.ts new file mode 100755 index 000000000000..9f8b2e7d39a5 --- /dev/null +++ b/testing/_test_utils.ts @@ -0,0 +1,18 @@ +export class Point { + constructor(public x: number, public y: number) {} + // deno-lint-ignore no-explicit-any + action(...args: any[]): any { + return args[0]; + } + toString(): string { + return [this.x, this.y].join(", "); + } + *[Symbol.iterator](): IterableIterator { + yield this.x; + yield this.y; + } +} + +export function stringifyPoint(point: Point) { + return point.toString(); +} diff --git a/testing/mock.ts b/testing/mock.ts new file mode 100644 index 000000000000..75e8f0656750 --- /dev/null +++ b/testing/mock.ts @@ -0,0 +1,831 @@ +/** This module is browser compatible. */ + +import { + assertEquals, + AssertionError, + assertIsError, + assertRejects, +} from "./asserts.ts"; + +/** An error related to spying on a function or instance method. */ +export class MockError extends Error { + constructor(message: string) { + super(message); + this.name = "MockError"; + } +} + +/** Call information recorded by a spy. */ +export interface SpyCall< + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], + // deno-lint-ignore no-explicit-any + Return = any, +> { + /** Arguments passed to a function when called. */ + args: Args; + /** The value that was returned by a function. */ + returned?: Return; + /** The error value that was thrown by a function. */ + error?: Error; + /** The instance that a method was called on. */ + self?: Self; +} + +/** A function or instance method wrapper that records all calls made to it. */ +export interface Spy< + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], + // deno-lint-ignore no-explicit-any + Return = any, +> { + (this: Self, ...args: Args): Return; + /** The function that is being spied on. */ + original: (this: Self, ...args: Args) => Return; + /** Information about calls made to the function or instance method. */ + calls: SpyCall[]; + /** Whether or not the original instance method has been restored. */ + restored: boolean; + /** If spying on an instance method, this restores the original instance method. */ + restore(): void; +} + +/** Wraps a function with a Spy. */ +function functionSpy< + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], + Return = undefined, +>(): Spy; +function functionSpy< + Self, + Args extends unknown[], + Return, +>(func: (this: Self, ...args: Args) => Return): Spy; +function functionSpy< + Self, + Args extends unknown[], + Return, +>(func?: (this: Self, ...args: Args) => Return): Spy { + const original = func ?? (() => {}) as (this: Self, ...args: Args) => Return, + calls: SpyCall[] = []; + const spy = function (this: Self, ...args: Args): Return { + const call: SpyCall = { args }; + if (this) call.self = this; + try { + call.returned = original.apply(this, args); + } catch (error) { + call.error = error as Error; + calls.push(call); + throw error; + } + calls.push(call); + return call.returned; + } as Spy; + Object.defineProperties(spy, { + original: { + enumerable: true, + value: original, + }, + calls: { + enumerable: true, + value: calls, + }, + restored: { + enumerable: true, + get: () => false, + }, + restore: { + enumerable: true, + value: () => { + throw new MockError("function cannot be restored"); + }, + }, + }); + return spy; +} + +/** Checks if a function is a spy. */ +function isSpy( + func: ((this: Self, ...args: Args) => Return) | unknown, +): func is Spy { + const spy = func as Spy; + return typeof spy === "function" && + typeof spy.original === "function" && + typeof spy.restored === "boolean" && + typeof spy.restore === "function" && + Array.isArray(spy.calls); +} + +// deno-lint-ignore no-explicit-any +const sessions: Set>[] = []; +// deno-lint-ignore no-explicit-any +function getSession(): Set> { + if (sessions.length === 0) sessions.push(new Set()); + return sessions[sessions.length - 1]; +} +// deno-lint-ignore no-explicit-any +function registerMock(spy: Spy): void { + const session = getSession(); + session.add(spy); +} +// deno-lint-ignore no-explicit-any +function unregisterMock(spy: Spy): void { + const session = getSession(); + session.delete(spy); +} + +/** + * Creates a session that tracks all mocks created before it's restored. + * If a callback is provided, it restores all mocks created within it. + */ +export function mockSession(): number; +export function mockSession< + Self, + Args extends unknown[], + Return, +>( + func: (this: Self, ...args: Args) => Return, +): (this: Self, ...args: Args) => Return; +export function mockSession< + Self, + Args extends unknown[], + Return, +>( + func?: (this: Self, ...args: Args) => Return, +): number | ((this: Self, ...args: Args) => Return) { + if (func) { + return function (this: Self, ...args: Args): Return { + const id = sessions.length; + sessions.push(new Set()); + try { + return func.apply(this, args); + } finally { + restore(id); + } + }; + } else { + sessions.push(new Set()); + return sessions.length - 1; + } +} + +/** Creates an async session that tracks all mocks created before the promise resolves. */ +export function mockSessionAsync< + Self, + Args extends unknown[], + Return, +>( + func: (this: Self, ...args: Args) => Promise, +): (this: Self, ...args: Args) => Promise { + return async function (this: Self, ...args: Args): Promise { + const id = sessions.length; + sessions.push(new Set()); + try { + return await func.apply(this, args); + } finally { + restore(id); + } + }; +} + +/** + * Restores all mocks registered in the current session that have not already been restored. + * If an id is provided, it will restore all mocks registered in the session associed with that id that have not already been restored. + */ +export function restore(id?: number): void { + id ??= (sessions.length || 1) - 1; + while (id < sessions.length) { + const session = sessions.pop(); + if (session) { + for (const value of session) { + value.restore(); + } + } + } +} + +/** Wraps an instance method with a Spy. */ +function methodSpy< + Self, + Args extends unknown[], + Return, +>(self: Self, property: keyof Self): Spy { + if (typeof self[property] !== "function") { + throw new MockError("property is not an instance method"); + } + if (isSpy(self[property])) { + throw new MockError("already spying on instance method"); + } + + const propertyDescriptor = Object.getOwnPropertyDescriptor(self, property); + if (propertyDescriptor && !propertyDescriptor.configurable) { + throw new MockError("cannot spy on non configurable instance method"); + } + + const original = self[property] as unknown as ( + this: Self, + ...args: Args + ) => Return, + calls: SpyCall[] = []; + let restored = false; + const spy = function (this: Self, ...args: Args): Return { + const call: SpyCall = { args }; + if (this) call.self = this; + try { + call.returned = original.apply(this, args); + } catch (error) { + call.error = error as Error; + calls.push(call); + throw error; + } + calls.push(call); + return call.returned; + } as Spy; + Object.defineProperties(spy, { + original: { + enumerable: true, + value: original, + }, + calls: { + enumerable: true, + value: calls, + }, + restored: { + enumerable: true, + get: () => restored, + }, + restore: { + enumerable: true, + value: () => { + if (restored) { + throw new MockError("instance method already restored"); + } + if (propertyDescriptor) { + Object.defineProperty(self, property, propertyDescriptor); + } else { + delete self[property]; + } + restored = true; + unregisterMock(spy); + }, + }, + }); + + Object.defineProperty(self, property, { + configurable: true, + enumerable: propertyDescriptor?.enumerable, + writable: propertyDescriptor?.writable, + value: spy, + }); + + registerMock(spy); + return spy; +} + +/** Wraps a function or instance method with a Spy. */ +export function spy< + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], + Return = undefined, +>(): Spy; +export function spy< + Self, + Args extends unknown[], + Return, +>(func: (this: Self, ...args: Args) => Return): Spy; +export function spy< + Self, + Args extends unknown[], + Return, +>(self: Self, property: keyof Self): Spy; +export function spy< + Self, + Args extends unknown[], + Return, +>( + funcOrSelf?: ((this: Self, ...args: Args) => Return) | Self, + property?: keyof Self, +): Spy { + const spy = typeof property !== "undefined" + ? methodSpy(funcOrSelf as Self, property) + : typeof funcOrSelf === "function" + ? functionSpy( + funcOrSelf as (this: Self, ...args: Args) => Return, + ) + : functionSpy(); + return spy; +} + +/** An instance method replacement that records all calls made to it. */ +export interface Stub< + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], + // deno-lint-ignore no-explicit-any + Return = any, +> extends Spy { + /** The function that is used instead of the original. */ + fake: (this: Self, ...args: Args) => Return; +} + +/** Replaces an instance method with a Stub. */ +export function stub< + Self, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], + Return = undefined, +>(self: Self, property: keyof Self): Stub; +export function stub< + Self, + Args extends unknown[], + Return, +>( + self: Self, + property: keyof Self, + func: (this: Self, ...args: Args) => Return, +): Stub; +export function stub< + Self, + Args extends unknown[], + Return, +>( + self: Self, + property: keyof Self, + func?: (this: Self, ...args: Args) => Return, +): Stub { + if (typeof self[property] !== "function") { + throw new MockError("property is not an instance method"); + } + if (isSpy(self[property])) { + throw new MockError("already spying on instance method"); + } + + const propertyDescriptor = Object.getOwnPropertyDescriptor(self, property); + if (propertyDescriptor && !propertyDescriptor.configurable) { + throw new MockError("cannot spy on non configurable instance method"); + } + + const fake = func ?? (() => {}) as (this: Self, ...args: Args) => Return; + + const original = self[property] as unknown as ( + this: Self, + ...args: Args + ) => Return, + calls: SpyCall[] = []; + let restored = false; + const stub = function (this: Self, ...args: Args): Return { + const call: SpyCall = { args }; + if (this) call.self = this; + try { + call.returned = fake.apply(this, args); + } catch (error) { + call.error = error as Error; + calls.push(call); + throw error; + } + calls.push(call); + return call.returned; + } as Stub; + Object.defineProperties(stub, { + original: { + enumerable: true, + value: original, + }, + fake: { + enumerable: true, + value: fake, + }, + calls: { + enumerable: true, + value: calls, + }, + restored: { + enumerable: true, + get: () => restored, + }, + restore: { + enumerable: true, + value: () => { + if (restored) { + throw new MockError("instance method already restored"); + } + if (propertyDescriptor) { + Object.defineProperty(self, property, propertyDescriptor); + } else { + delete self[property]; + } + restored = true; + unregisterMock(stub); + }, + }, + }); + + Object.defineProperty(self, property, { + configurable: true, + enumerable: propertyDescriptor?.enumerable, + writable: propertyDescriptor?.writable, + value: stub, + }); + + registerMock(stub); + return stub; +} + +/** + * Asserts that a spy is called as much as expected and no more. + */ +export function assertSpyCalls< + Self, + Args extends unknown[], + Return, +>( + spy: Spy, + expectedCalls: number, +) { + try { + assertEquals(spy.calls.length, expectedCalls); + } catch (e) { + assertIsError(e); + let message = spy.calls.length < expectedCalls + ? "spy not called as much as expected:\n" + : "spy called more than expected:\n"; + message += e.message.split("\n").slice(1).join("\n"); + throw new AssertionError(message); + } +} + +/** Call information recorded by a spy. */ +export interface ExpectedSpyCall< + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], + // deno-lint-ignore no-explicit-any + Return = any, +> { + /** Arguments passed to a function when called. */ + args?: [...Args, ...unknown[]]; + /** The instance that a method was called on. */ + self?: Self; + /** + * The value that was returned by a function. + * If you expect a promise to reject, expect error instead. + */ + returned?: Return; + error?: { + /** The class for the error that was thrown by a function. */ + // deno-lint-ignore no-explicit-any + Class?: new (...args: any[]) => Error; + /** Part of the message for the error that was thrown by a function. */ + msgIncludes?: string; + }; +} + +/** + * Asserts that a spy is called as expected. + */ +export function assertSpyCall< + Self, + Args extends unknown[], + Return, +>( + spy: Spy, + callIndex: number, + expected?: ExpectedSpyCall, +) { + if (spy.calls.length < (callIndex + 1)) { + throw new AssertionError("spy not called as much as expected"); + } + const call: SpyCall = spy.calls[callIndex]; + if (expected) { + if (expected.args) { + try { + assertEquals(call.args, expected.args); + } catch (e) { + assertIsError(e); + throw new AssertionError( + "spy not called with expected args:\n" + + e.message.split("\n").slice(1).join("\n"), + ); + } + } + + if ("self" in expected) { + try { + assertEquals(call.self, expected.self); + } catch (e) { + assertIsError(e); + let message = expected.self + ? "spy not called as method on expected self:\n" + : "spy not expected to be called as method on object:\n"; + message += e.message.split("\n").slice(1).join("\n"); + throw new AssertionError(message); + } + } + + if ("returned" in expected) { + if ("error" in expected) { + throw new TypeError( + "do not expect error and return, only one should be expected", + ); + } + if (call.error) { + throw new AssertionError( + "spy call did not return expected value, an error was thrown.", + ); + } + try { + assertEquals(call.returned, expected.returned); + } catch (e) { + assertIsError(e); + throw new AssertionError( + "spy call did not return expected value:\n" + + e.message.split("\n").slice(1).join("\n"), + ); + } + } + + if ("error" in expected) { + if ("returned" in call) { + throw new AssertionError( + "spy call did not throw an error, a value was returned.", + ); + } + assertIsError( + call.error, + expected.error?.Class, + expected.error?.msgIncludes, + ); + } + } +} + +/** + * Asserts that an async spy is called as expected. + */ +export async function assertSpyCallAsync< + Self, + Args extends unknown[], + Return, +>( + spy: Spy>, + callIndex: number, + expected?: ExpectedSpyCall | Return>, +) { + const expectedSync = expected && { ...expected }; + if (expectedSync) { + delete expectedSync.returned; + delete expectedSync.error; + } + assertSpyCall(spy, callIndex, expectedSync); + const call = spy.calls[callIndex]; + + if (call.error) { + throw new AssertionError( + "spy call did not return a promise, an error was thrown.", + ); + } + if (call.returned !== Promise.resolve(call.returned)) { + throw new AssertionError( + "spy call did not return a promise, a value was returned.", + ); + } + + if (expected) { + if ("returned" in expected) { + if ("error" in expected) { + throw new TypeError( + "do not expect error and return, only one should be expected", + ); + } + if (call.error) { + throw new AssertionError( + "spy call did not return expected value, an error was thrown.", + ); + } + let expectedResolved; + try { + expectedResolved = await expected.returned; + } catch { + throw new TypeError( + "do not expect rejected promise, expect error instead", + ); + } + + let resolved; + try { + resolved = await call.returned; + } catch { + throw new AssertionError("spy call returned promise was rejected"); + } + + try { + assertEquals(resolved, expectedResolved); + } catch (e) { + assertIsError(e); + throw new AssertionError( + "spy call did not resolve to expected value:\n" + + e.message.split("\n").slice(1).join("\n"), + ); + } + } + + if ("error" in expected) { + await assertRejects( + () => Promise.resolve(call.returned), + expected.error?.Class ?? Error, + expected.error?.msgIncludes ?? "", + ); + } + } +} + +/** + * Asserts that a spy is called with a specific arg as expected. + */ +export function assertSpyCallArg< + Self, + Args extends unknown[], + Return, + ExpectedArg, +>( + spy: Spy, + callIndex: number, + argIndex: number, + expected: ExpectedArg, +): ExpectedArg { + assertSpyCall(spy, callIndex); + const call = spy.calls[callIndex]; + const arg = call.args[argIndex]; + assertEquals(arg, expected); + return arg as ExpectedArg; +} + +/** + * Asserts that an spy is called with a specific range of args as expected. + * If a start and end index is not provided, the expected will be compared against all args. + * If a start is provided without an end index, the expected will be compared against all args from the start index to the end. + * The end index is not included in the range of args that are compared. + */ +export function assertSpyCallArgs< + Self, + Args extends unknown[], + Return, + ExpectedArgs extends unknown[], +>( + spy: Spy, + callIndex: number, + expected: ExpectedArgs, +): ExpectedArgs; +export function assertSpyCallArgs< + Self, + Args extends unknown[], + Return, + ExpectedArgs extends unknown[], +>( + spy: Spy, + callIndex: number, + argsStart: number, + expected: ExpectedArgs, +): ExpectedArgs; +export function assertSpyCallArgs< + Self, + Args extends unknown[], + Return, + ExpectedArgs extends unknown[], +>( + spy: Spy, + callIndex: number, + argStart: number, + argEnd: number, + expected: ExpectedArgs, +): ExpectedArgs; +export function assertSpyCallArgs< + ExpectedArgs extends unknown[], + Args extends unknown[], + Return, + Self, +>( + spy: Spy, + callIndex: number, + argsStart?: number | ExpectedArgs, + argsEnd?: number | ExpectedArgs, + expected?: ExpectedArgs, +): ExpectedArgs { + assertSpyCall(spy, callIndex); + const call = spy.calls[callIndex]; + if (!expected) { + expected = argsEnd as ExpectedArgs; + argsEnd = undefined; + } + if (!expected) { + expected = argsStart as ExpectedArgs; + argsStart = undefined; + } + const args = typeof argsEnd === "number" + ? call.args.slice(argsStart as number, argsEnd) + : typeof argsStart === "number" + ? call.args.slice(argsStart) + : call.args; + assertEquals(args, expected); + return args as ExpectedArgs; +} + +/** Creates a function that returns the instance the method was called on. */ +export function returnsThis< + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], +>(): (this: Self, ...args: Args) => Self { + return function (this: Self): Self { + return this; + }; +} + +/** Creates a function that returns one of its arguments. */ +// deno-lint-ignore no-explicit-any +export function returnsArg( + idx: number, +): (this: Self, ...args: Arg[]) => Arg { + return function (...args: Arg[]): Arg { + return args[idx]; + }; +} + +/** Creates a function that returns its arguments or a subset of them. If end is specified, it will return arguments up to but not including the end. */ +export function returnsArgs< + Args extends unknown[], + // deno-lint-ignore no-explicit-any + Self = any, +>( + start = 0, + end?: number, +): (this: Self, ...args: Args) => Args { + return function (this: Self, ...args: Args): Args { + return args.slice(start, end) as Args; + }; +} + +/** Creates a function that returns the iterable values. Any iterable values that are errors will be thrown. */ +export function returnsNext< + Return, + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], +>( + values: Iterable, +): (this: Self, ...args: Args) => Return { + const gen = (function* returnsValue() { + yield* values; + })(); + let calls = 0; + return function () { + const next = gen.next(); + if (next.done) { + throw new MockError(`not expected to be called more than ${calls} times`); + } + calls++; + const { value } = next; + if (value instanceof Error) throw value; + return value; + }; +} + +/** Creates a function that resolves the awaited iterable values. Any awaited iterable values that are errors will be thrown. */ +export function resolvesNext< + Return, + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], +>( + iterable: + | Iterable> + | AsyncIterable>, +): (this: Self, ...args: Args) => Promise { + const gen = (async function* returnsValue() { + yield* iterable; + })(); + let calls = 0; + return async function () { + const next = await gen.next(); + if (next.done) { + throw new MockError(`not expected to be called more than ${calls} times`); + } + calls++; + const { value } = next; + if (value instanceof Error) throw value; + return value; + }; +} diff --git a/testing/mock_examples/internals_injection.ts b/testing/mock_examples/internals_injection.ts new file mode 100644 index 000000000000..43844129dd3e --- /dev/null +++ b/testing/mock_examples/internals_injection.ts @@ -0,0 +1,9 @@ +export function multiply(a: number, b: number): number { + return a * b; +} + +export function square(value: number): number { + return _internals.multiply(value, value); +} + +export const _internals = { multiply }; diff --git a/testing/mock_examples/internals_injection_test.ts b/testing/mock_examples/internals_injection_test.ts new file mode 100644 index 000000000000..14b939b71bba --- /dev/null +++ b/testing/mock_examples/internals_injection_test.ts @@ -0,0 +1,23 @@ +import { assertSpyCall, assertSpyCalls, spy } from "../mock.ts"; +import { assertEquals } from "../asserts.ts"; +import { _internals, square } from "./internals_injection.ts"; + +Deno.test("square calls multiply and returns results", () => { + const multiplySpy = spy(_internals, "multiply"); + + try { + assertEquals(square(5), 25); + } finally { + // unwraps the multiply method on the _internals object + multiplySpy.restore(); + } + + // asserts that multiplySpy was called at least once and details about the first call. + assertSpyCall(multiplySpy, 0, { + args: [5, 5], + returned: 25, + }); + + // asserts that multiplySpy was only called once. + assertSpyCalls(multiplySpy, 1); +}); diff --git a/testing/mock_examples/parameter_injection.ts b/testing/mock_examples/parameter_injection.ts new file mode 100644 index 000000000000..64aee2e173f2 --- /dev/null +++ b/testing/mock_examples/parameter_injection.ts @@ -0,0 +1,10 @@ +export function multiply(a: number, b: number): number { + return a * b; +} + +export function square( + multiplyFn: (a: number, b: number) => number, + value: number, +): number { + return multiplyFn(value, value); +} diff --git a/testing/mock_examples/parameter_injection_test.ts b/testing/mock_examples/parameter_injection_test.ts new file mode 100644 index 000000000000..1ec580ea27af --- /dev/null +++ b/testing/mock_examples/parameter_injection_test.ts @@ -0,0 +1,18 @@ +import { assertSpyCall, assertSpyCalls, spy } from "../mock.ts"; +import { assertEquals } from "../asserts.ts"; +import { multiply, square } from "./parameter_injection.ts"; + +Deno.test("square calls multiply and returns results", () => { + const multiplySpy = spy(multiply); + + assertEquals(square(multiplySpy, 5), 25); + + // asserts that multiplySpy was called at least once and details about the first call. + assertSpyCall(multiplySpy, 0, { + args: [5, 5], + returned: 25, + }); + + // asserts that multiplySpy was only called once. + assertSpyCalls(multiplySpy, 1); +}); diff --git a/testing/mock_examples/random.ts b/testing/mock_examples/random.ts new file mode 100644 index 000000000000..06e98a345de1 --- /dev/null +++ b/testing/mock_examples/random.ts @@ -0,0 +1,9 @@ +export function randomInt(lowerBound: number, upperBound: number): number { + return lowerBound + Math.floor(Math.random() * (upperBound - lowerBound)); +} + +export function randomMultiple(value: number): number { + return value * _internals.randomInt(-10, 10); +} + +export const _internals = { randomInt }; diff --git a/testing/mock_examples/random_test.ts b/testing/mock_examples/random_test.ts new file mode 100644 index 000000000000..8558629072e6 --- /dev/null +++ b/testing/mock_examples/random_test.ts @@ -0,0 +1,29 @@ +import { assertSpyCall, assertSpyCalls, returnsNext, stub } from "../mock.ts"; +import { assertEquals } from "../asserts.ts"; +import { _internals, randomMultiple } from "./random.ts"; + +Deno.test("randomMultiple uses randomInt to generate random multiples between -10 and 10 times the value", () => { + const randomIntStub = stub(_internals, "randomInt", returnsNext([-3, 3])); + + try { + assertEquals(randomMultiple(5), -15); + assertEquals(randomMultiple(5), 15); + } finally { + // unwraps the randomInt method on the _internals object + randomIntStub.restore(); + } + + // asserts that randomIntStub was called at least once and details about the first call. + assertSpyCall(randomIntStub, 0, { + args: [-10, 10], + returned: -3, + }); + // asserts that randomIntStub was called at least twice and details about the second call. + assertSpyCall(randomIntStub, 1, { + args: [-10, 10], + returned: 3, + }); + + // asserts that randomIntStub was only called twice. + assertSpyCalls(randomIntStub, 2); +}); diff --git a/testing/mock_test.ts b/testing/mock_test.ts new file mode 100644 index 000000000000..7cf1f6ef0131 --- /dev/null +++ b/testing/mock_test.ts @@ -0,0 +1,1783 @@ +import { delay } from "../async/delay.ts"; +import { + assertEquals, + AssertionError, + assertNotEquals, + assertRejects, + assertThrows, +} from "./asserts.ts"; +import { + assertSpyCall, + assertSpyCallArg, + assertSpyCallArgs, + assertSpyCallAsync, + assertSpyCalls, + MockError, + mockSession, + mockSessionAsync, + resolvesNext, + restore, + returnsArg, + returnsArgs, + returnsNext, + returnsThis, + Spy, + spy, + stub, +} from "./mock.ts"; +import { Point, stringifyPoint } from "./_test_utils.ts"; + +Deno.test("spy default", () => { + const func = spy(); + assertSpyCalls(func, 0); + + assertEquals(func(), undefined); + assertSpyCall(func, 0, { + self: undefined, + args: [], + returned: undefined, + }); + assertSpyCalls(func, 1); + + assertEquals(func("x"), undefined); + assertSpyCall(func, 1, { + self: undefined, + args: ["x"], + returned: undefined, + }); + assertSpyCalls(func, 2); + + assertEquals(func({ x: 3 }), undefined); + assertSpyCall(func, 2, { + self: undefined, + args: [{ x: 3 }], + returned: undefined, + }); + assertSpyCalls(func, 3); + + assertEquals(func(3, 5, 7), undefined); + assertSpyCall(func, 3, { + self: undefined, + args: [3, 5, 7], + returned: undefined, + }); + assertSpyCalls(func, 4); + + const point: Point = new Point(2, 3); + assertEquals(func(Point, stringifyPoint, point), undefined); + assertSpyCall(func, 4, { + self: undefined, + args: [Point, stringifyPoint, point], + returned: undefined, + }); + assertSpyCalls(func, 5); + + assertEquals(func.restored, false); + assertThrows( + () => func.restore(), + MockError, + "function cannot be restore", + ); + assertEquals(func.restored, false); +}); + +Deno.test("spy function", () => { + const func = spy((value) => value); + assertSpyCalls(func, 0); + + assertEquals(func(undefined), undefined); + assertSpyCall(func, 0, { + self: undefined, + args: [undefined], + returned: undefined, + }); + assertSpyCalls(func, 1); + + assertEquals(func("x"), "x"); + assertSpyCall(func, 1, { + self: undefined, + args: ["x"], + returned: "x", + }); + assertSpyCalls(func, 2); + + assertEquals(func({ x: 3 }), { x: 3 }); + assertSpyCall(func, 2, { + self: undefined, + args: [{ x: 3 }], + returned: { x: 3 }, + }); + assertSpyCalls(func, 3); + + const point = new Point(2, 3); + assertEquals(func(point), point); + assertSpyCall(func, 3, { + self: undefined, + args: [point], + returned: point, + }); + assertSpyCalls(func, 4); + + assertEquals(func.restored, false); + assertThrows( + () => func.restore(), + MockError, + "function cannot be restored", + ); + assertEquals(func.restored, false); +}); + +Deno.test("spy instance method", () => { + const point = new Point(2, 3); + const func = spy(point, "action"); + assertSpyCalls(func, 0); + + assertEquals(func.call(point), undefined); + assertSpyCall(func, 0, { + self: point, + args: [], + returned: undefined, + }); + assertSpyCalls(func, 1); + + assertEquals(point.action(), undefined); + assertSpyCall(func, 1, { self: point, args: [] }); + assertSpyCalls(func, 2); + + assertEquals(func.call(point, "x"), "x"); + assertSpyCall(func, 2, { + self: point, + args: ["x"], + returned: "x", + }); + assertSpyCalls(func, 3); + + assertEquals(point.action("x"), "x"); + assertSpyCall(func, 3, { + self: point, + args: ["x"], + returned: "x", + }); + assertSpyCalls(func, 4); + + assertEquals(func.call(point, { x: 3 }), { x: 3 }); + assertSpyCall(func, 4, { + self: point, + args: [{ x: 3 }], + returned: { x: 3 }, + }); + assertSpyCalls(func, 5); + + assertEquals(point.action({ x: 3 }), { x: 3 }); + assertSpyCall(func, 5, { + self: point, + args: [{ x: 3 }], + returned: { x: 3 }, + }); + assertSpyCalls(func, 6); + + assertEquals(func.call(point, 3, 5, 7), 3); + assertSpyCall(func, 6, { + self: point, + args: [3, 5, 7], + returned: 3, + }); + assertSpyCalls(func, 7); + + assertEquals(point.action(3, 5, 7), 3); + assertSpyCall(func, 7, { + self: point, + args: [3, 5, 7], + returned: 3, + }); + assertSpyCalls(func, 8); + + assertEquals(func.call(point, Point, stringifyPoint, point), Point); + assertSpyCall(func, 8, { + self: point, + args: [Point, stringifyPoint, point], + returned: Point, + }); + assertSpyCalls(func, 9); + + assertEquals(point.action(Point, stringifyPoint, point), Point); + assertSpyCall(func, 9, { + self: point, + args: [Point, stringifyPoint, point], + returned: Point, + }); + assertSpyCalls(func, 10); + + assertNotEquals(func, Point.prototype.action); + assertEquals(point.action, func); + + assertEquals(func.restored, false); + func.restore(); + assertEquals(func.restored, true); + assertEquals(point.action, Point.prototype.action); + assertThrows( + () => func.restore(), + MockError, + "instance method already restored", + ); + assertEquals(func.restored, true); +}); + +Deno.test("spy instance method symbol", () => { + const point = new Point(2, 3); + const func = spy(point, Symbol.iterator); + assertSpyCalls(func, 0); + + const values: number[] = []; + for (const value of point) { + values.push(value); + } + assertSpyCall(func, 0, { + self: point, + args: [], + }); + assertSpyCalls(func, 1); + + assertEquals(values, [2, 3]); + assertEquals([...point], [2, 3]); + assertSpyCall(func, 1, { + self: point, + args: [], + }); + assertSpyCalls(func, 2); + + assertNotEquals(func, Point.prototype[Symbol.iterator]); + assertEquals(point[Symbol.iterator], func); + + assertEquals(func.restored, false); + func.restore(); + assertEquals(func.restored, true); + assertEquals(point[Symbol.iterator], Point.prototype[Symbol.iterator]); + assertThrows( + () => func.restore(), + MockError, + "instance method already restored", + ); + assertEquals(func.restored, true); +}); + +Deno.test("spy instance method property descriptor", () => { + const point = new Point(2, 3); + const actionDescriptor: PropertyDescriptor = { + configurable: true, + enumerable: false, + writable: false, + value: function (...args: unknown[]) { + return args[1]; + }, + }; + Object.defineProperty(point, "action", actionDescriptor); + const action = spy(point, "action"); + assertSpyCalls(action, 0); + + assertEquals(action.call(point), undefined); + assertSpyCall(action, 0, { + self: point, + args: [], + returned: undefined, + }); + assertSpyCalls(action, 1); + + assertEquals(point.action(), undefined); + assertSpyCall(action, 1, { + self: point, + args: [], + returned: undefined, + }); + assertSpyCalls(action, 2); + + assertEquals(action.call(point, "x", "y"), "y"); + assertSpyCall(action, 2, { + self: point, + args: ["x", "y"], + returned: "y", + }); + assertSpyCalls(action, 3); + + assertEquals(point.action("x", "y"), "y"); + assertSpyCall(action, 3, { + self: point, + args: ["x", "y"], + returned: "y", + }); + assertSpyCalls(action, 4); + + assertNotEquals(action, actionDescriptor.value); + assertEquals(point.action, action); + + assertEquals(action.restored, false); + action.restore(); + assertEquals(action.restored, true); + assertEquals(point.action, actionDescriptor.value); + assertEquals( + Object.getOwnPropertyDescriptor(point, "action"), + actionDescriptor, + ); + assertThrows( + () => action.restore(), + MockError, + "instance method already restored", + ); + assertEquals(action.restored, true); +}); + +Deno.test("stub default", () => { + const point = new Point(2, 3); + const func = stub(point, "action"); + + assertSpyCalls(func, 0); + + assertEquals(func.call(point), undefined); + assertSpyCall(func, 0, { + self: point, + args: [], + returned: undefined, + }); + assertSpyCalls(func, 1); + + assertEquals(point.action(), undefined); + assertSpyCall(func, 1, { + self: point, + args: [], + returned: undefined, + }); + assertSpyCalls(func, 2); + + assertEquals(func.original, Point.prototype.action); + assertEquals(point.action, func); + + assertEquals(func.restored, false); + func.restore(); + assertEquals(func.restored, true); + assertEquals(point.action, Point.prototype.action); + assertThrows( + () => func.restore(), + MockError, + "instance method already restored", + ); + assertEquals(func.restored, true); +}); + +Deno.test("stub function", () => { + const point = new Point(2, 3); + const returns = [1, "b", 2, "d"]; + const func = stub(point, "action", () => returns.shift()); + + assertSpyCalls(func, 0); + + assertEquals(func.call(point), 1); + assertSpyCall(func, 0, { + self: point, + args: [], + returned: 1, + }); + assertSpyCalls(func, 1); + + assertEquals(point.action(), "b"); + assertSpyCall(func, 1, { + self: point, + args: [], + returned: "b", + }); + assertSpyCalls(func, 2); + + assertEquals(func.original, Point.prototype.action); + assertEquals(point.action, func); + + assertEquals(func.restored, false); + func.restore(); + assertEquals(func.restored, true); + assertEquals(point.action, Point.prototype.action); + assertThrows( + () => func.restore(), + MockError, + "instance method already restored", + ); + assertEquals(func.restored, true); +}); + +Deno.test("mockSession and mockSessionAsync", async () => { + const points = Array(6).fill(undefined).map(() => new Point(2, 3)); + let actions: Spy[] = []; + function assertRestored(expected: boolean[]): void { + assertEquals(actions.map((action) => action.restored), expected); + } + await mockSessionAsync(async () => { + actions.push(spy(points[0], "action")); + assertRestored([false]); + await mockSessionAsync(async () => { + await Promise.resolve(); + actions.push(spy(points[1], "action")); + assertRestored([false, false]); + mockSession(() => { + actions.push(spy(points[2], "action")); + actions.push(spy(points[3], "action")); + assertRestored([false, false, false, false]); + })(); + actions.push(spy(points[4], "action")); + assertRestored([false, false, true, true, false]); + })(); + actions.push(spy(points[5], "action")); + assertRestored([false, true, true, true, true, false]); + })(); + assertRestored(Array(6).fill(true)); + restore(); + assertRestored(Array(6).fill(true)); + + actions = []; + mockSession(() => { + actions = points.map((point) => spy(point, "action")); + assertRestored(Array(6).fill(false)); + })(); + assertRestored(Array(6).fill(true)); + restore(); + assertRestored(Array(6).fill(true)); +}); + +Deno.test("mockSession and restore current session", () => { + const points = Array(6).fill(undefined).map(() => new Point(2, 3)); + let actions: Spy[]; + function assertRestored(expected: boolean[]): void { + assertEquals(actions.map((action) => action.restored), expected); + } + try { + actions = points.map((point) => spy(point, "action")); + + assertRestored(Array(6).fill(false)); + restore(); + assertRestored(Array(6).fill(true)); + restore(); + assertRestored(Array(6).fill(true)); + + actions = []; + try { + actions.push(spy(points[0], "action")); + try { + mockSession(); + actions.push(spy(points[1], "action")); + try { + mockSession(); + actions.push(spy(points[2], "action")); + actions.push(spy(points[3], "action")); + } finally { + assertRestored([false, false, false, false]); + restore(); + } + actions.push(spy(points[4], "action")); + } finally { + assertRestored([false, false, true, true, false]); + restore(); + } + actions.push(spy(points[5], "action")); + } finally { + assertRestored([false, true, true, true, true, false]); + restore(); + } + assertRestored(Array(6).fill(true)); + restore(); + assertRestored(Array(6).fill(true)); + + actions = points.map((point) => spy(point, "action")); + assertRestored(Array(6).fill(false)); + restore(); + assertRestored(Array(6).fill(true)); + restore(); + assertRestored(Array(6).fill(true)); + } finally { + restore(); + } +}); + +Deno.test("mockSession and restore multiple sessions", () => { + const points = Array(6).fill(undefined).map(() => new Point(2, 3)); + let actions: Spy[]; + function assertRestored(expected: boolean[]): void { + assertEquals(actions.map((action) => action.restored), expected); + } + try { + actions = []; + try { + actions.push(spy(points[0], "action")); + const id = mockSession(); + try { + actions.push(spy(points[1], "action")); + actions.push(spy(points[2], "action")); + mockSession(); + actions.push(spy(points[3], "action")); + actions.push(spy(points[4], "action")); + } finally { + assertRestored([false, false, false, false, false]); + restore(id); + } + actions.push(spy(points[5], "action")); + } finally { + assertRestored([false, true, true, true, true, false]); + restore(); + } + assertRestored(Array(6).fill(true)); + restore(); + assertRestored(Array(6).fill(true)); + } finally { + restore(); + } +}); + +Deno.test("assertSpyCalls", () => { + const spyFunc = spy(); + + assertSpyCalls(spyFunc, 0); + assertThrows( + () => assertSpyCalls(spyFunc, 1), + AssertionError, + "spy not called as much as expected", + ); + + spyFunc(); + assertSpyCalls(spyFunc, 1); + assertThrows( + () => assertSpyCalls(spyFunc, 0), + AssertionError, + "spy called more than expected", + ); + assertThrows( + () => assertSpyCalls(spyFunc, 2), + AssertionError, + "spy not called as much as expected", + ); +}); + +Deno.test("assertSpyCall function", () => { + const spyFunc = spy((multiplier?: number) => 5 * (multiplier ?? 1)); + + assertThrows( + () => assertSpyCall(spyFunc, 0), + AssertionError, + "spy not called as much as expected", + ); + + spyFunc(); + assertSpyCall(spyFunc, 0); + assertSpyCall(spyFunc, 0, { + args: [], + self: undefined, + returned: 5, + }); + assertSpyCall(spyFunc, 0, { + args: [], + }); + assertSpyCall(spyFunc, 0, { + self: undefined, + }); + assertSpyCall(spyFunc, 0, { + returned: 5, + }); + + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + args: [1], + self: {}, + returned: 2, + }), + AssertionError, + "spy not called with expected args", + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + args: [1], + }), + AssertionError, + "spy not called with expected args", + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + self: {}, + }), + AssertionError, + "spy not called as method on expected self", + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + returned: 2, + }), + AssertionError, + "spy call did not return expected value", + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + error: { msgIncludes: "x" }, + }), + AssertionError, + "spy call did not throw an error, a value was returned.", + ); + assertThrows( + () => assertSpyCall(spyFunc, 1), + AssertionError, + "spy not called as much as expected", + ); +}); + +Deno.test("assertSpyCall method", () => { + const point = new Point(2, 3); + const spyMethod = spy(point, "action"); + + assertThrows( + () => assertSpyCall(spyMethod, 0), + AssertionError, + "spy not called as much as expected", + ); + + point.action(3, 7); + assertSpyCall(spyMethod, 0); + assertSpyCall(spyMethod, 0, { + args: [3, 7], + self: point, + returned: 3, + }); + assertSpyCall(spyMethod, 0, { + args: [3, 7], + }); + assertSpyCall(spyMethod, 0, { + self: point, + }); + assertSpyCall(spyMethod, 0, { + returned: 3, + }); + + assertThrows( + () => + assertSpyCall(spyMethod, 0, { + args: [7, 4], + self: undefined, + returned: 7, + }), + AssertionError, + "spy not called with expected args", + ); + assertThrows( + () => + assertSpyCall(spyMethod, 0, { + args: [7, 3], + }), + AssertionError, + "spy not called with expected args", + ); + assertThrows( + () => + assertSpyCall(spyMethod, 0, { + self: undefined, + }), + AssertionError, + "spy not expected to be called as method on object", + ); + assertThrows( + () => + assertSpyCall(spyMethod, 0, { + returned: 7, + }), + AssertionError, + "spy call did not return expected value", + ); + assertThrows( + () => assertSpyCall(spyMethod, 1), + AssertionError, + "spy not called as much as expected", + ); + + spyMethod.call(point, 9); + assertSpyCall(spyMethod, 1); + assertSpyCall(spyMethod, 1, { + args: [9], + self: point, + returned: 9, + }); + assertSpyCall(spyMethod, 1, { + args: [9], + }); + assertSpyCall(spyMethod, 1, { + self: point, + }); + assertSpyCall(spyMethod, 1, { + returned: 9, + }); + + assertThrows( + () => + assertSpyCall(spyMethod, 1, { + args: [7, 4], + self: point, + returned: 7, + }), + AssertionError, + "spy not called with expected args", + ); + assertThrows( + () => + assertSpyCall(spyMethod, 1, { + args: [7, 3], + }), + AssertionError, + "spy not called with expected args", + ); + assertThrows( + () => + assertSpyCall(spyMethod, 1, { + self: new Point(1, 2), + }), + AssertionError, + "spy not called as method on expected self", + ); + assertThrows( + () => + assertSpyCall(spyMethod, 1, { + returned: 7, + }), + AssertionError, + "spy call did not return expected value", + ); + assertThrows( + () => + assertSpyCall(spyMethod, 1, { + error: { msgIncludes: "x" }, + }), + AssertionError, + "spy call did not throw an error, a value was returned.", + ); + assertThrows( + () => assertSpyCall(spyMethod, 2), + AssertionError, + "spy not called as much as expected", + ); +}); + +class ExampleError extends Error {} +class OtherError extends Error {} + +Deno.test("assertSpyCall error", () => { + const spyFunc = spy((_value?: number) => { + throw new ExampleError("failed"); + }); + + assertThrows(() => spyFunc(), ExampleError, "fail"); + assertSpyCall(spyFunc, 0); + assertSpyCall(spyFunc, 0, { + args: [], + self: undefined, + error: { + Class: ExampleError, + msgIncludes: "fail", + }, + }); + assertSpyCall(spyFunc, 0, { + args: [], + }); + assertSpyCall(spyFunc, 0, { + self: undefined, + }); + assertSpyCall(spyFunc, 0, { + error: { + Class: ExampleError, + msgIncludes: "fail", + }, + }); + assertSpyCall(spyFunc, 0, { + error: { + Class: Error, + msgIncludes: "fail", + }, + }); + + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + args: [1], + self: {}, + error: { + Class: OtherError, + msgIncludes: "fail", + }, + }), + AssertionError, + "spy not called with expected args", + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + args: [1], + }), + AssertionError, + "spy not called with expected args", + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + self: {}, + }), + AssertionError, + "spy not called as method on expected self", + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + error: { + Class: OtherError, + msgIncludes: "fail", + }, + }), + AssertionError, + 'Expected error to be instance of "OtherError", but was "ExampleError".', + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + error: { + Class: OtherError, + msgIncludes: "x", + }, + }), + AssertionError, + 'Expected error to be instance of "OtherError", but was "ExampleError".', + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + error: { + Class: ExampleError, + msgIncludes: "x", + }, + }), + AssertionError, + 'Expected error message to include "x", but got "failed".', + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + error: { + Class: Error, + msgIncludes: "x", + }, + }), + AssertionError, + 'Expected error message to include "x", but got "failed".', + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + error: { + msgIncludes: "x", + }, + }), + AssertionError, + 'Expected error message to include "x", but got "failed".', + ); + assertThrows( + () => + assertSpyCall(spyFunc, 0, { + returned: 7, + }), + AssertionError, + "spy call did not return expected value, an error was thrown.", + ); + assertThrows( + () => assertSpyCall(spyFunc, 1), + AssertionError, + "spy not called as much as expected", + ); +}); + +Deno.test("assertSpyCallAsync function", async () => { + const spyFunc = spy((multiplier?: number) => + Promise.resolve(5 * (multiplier ?? 1)) + ); + + await assertRejects( + () => assertSpyCallAsync(spyFunc, 0), + AssertionError, + "spy not called as much as expected", + ); + + await spyFunc(); + await assertSpyCallAsync(spyFunc, 0); + await assertSpyCallAsync(spyFunc, 0, { + args: [], + self: undefined, + returned: 5, + }); + await assertSpyCallAsync(spyFunc, 0, { + args: [], + self: undefined, + returned: Promise.resolve(5), + }); + await assertSpyCallAsync(spyFunc, 0, { + args: [], + }); + await assertSpyCallAsync(spyFunc, 0, { + self: undefined, + }); + await assertSpyCallAsync(spyFunc, 0, { + returned: Promise.resolve(5), + }); + + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + args: [1], + self: {}, + returned: 2, + }), + AssertionError, + "spy not called with expected args", + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + args: [1], + }), + AssertionError, + "spy not called with expected args", + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + self: {}, + }), + AssertionError, + "spy not called as method on expected self", + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + returned: 2, + }), + AssertionError, + "spy call did not resolve to expected value", + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + returned: Promise.resolve(2), + }), + AssertionError, + "spy call did not resolve to expected value", + ); + await assertRejects( + () => assertSpyCallAsync(spyFunc, 1), + AssertionError, + "spy not called as much as expected", + ); +}); + +Deno.test("assertSpyCallAsync method", async () => { + const point: Point = new Point(2, 3); + const spyMethod = stub( + point, + "action", + (x?: number, _y?: number) => Promise.resolve(x), + ); + + await assertRejects( + () => assertSpyCallAsync(spyMethod, 0), + AssertionError, + "spy not called as much as expected", + ); + + await point.action(3, 7); + await assertSpyCallAsync(spyMethod, 0); + await assertSpyCallAsync(spyMethod, 0, { + args: [3, 7], + self: point, + returned: 3, + }); + await assertSpyCallAsync(spyMethod, 0, { + args: [3, 7], + self: point, + returned: Promise.resolve(3), + }); + await assertSpyCallAsync(spyMethod, 0, { + args: [3, 7], + }); + await assertSpyCallAsync(spyMethod, 0, { + self: point, + }); + await assertSpyCallAsync(spyMethod, 0, { + returned: 3, + }); + await assertSpyCallAsync(spyMethod, 0, { + returned: Promise.resolve(3), + }); + + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 0, { + args: [7, 4], + self: undefined, + returned: 7, + }), + AssertionError, + "spy not called with expected args", + ); + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 0, { + args: [7, 3], + }), + AssertionError, + "spy not called with expected args", + ); + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 0, { + self: undefined, + }), + AssertionError, + "spy not expected to be called as method on object", + ); + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 0, { + returned: 7, + }), + AssertionError, + "spy call did not resolve to expected value", + ); + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 0, { + returned: Promise.resolve(7), + }), + AssertionError, + "spy call did not resolve to expected value", + ); + await assertRejects( + () => assertSpyCallAsync(spyMethod, 1), + AssertionError, + "spy not called as much as expected", + ); + + await spyMethod.call(point, 9); + await assertSpyCallAsync(spyMethod, 1); + await assertSpyCallAsync(spyMethod, 1, { + args: [9], + self: point, + returned: 9, + }); + await assertSpyCallAsync(spyMethod, 1, { + args: [9], + self: point, + returned: Promise.resolve(9), + }); + await assertSpyCallAsync(spyMethod, 1, { + args: [9], + }); + await assertSpyCallAsync(spyMethod, 1, { + self: point, + }); + await assertSpyCallAsync(spyMethod, 1, { + returned: 9, + }); + await assertSpyCallAsync(spyMethod, 1, { + returned: Promise.resolve(9), + }); + + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 1, { + args: [7, 4], + self: point, + returned: 7, + }), + AssertionError, + "spy not called with expected args", + ); + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 1, { + args: [7, 3], + }), + AssertionError, + "spy not called with expected args", + ); + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 1, { + self: new Point(1, 2), + }), + AssertionError, + "spy not called as method on expected self", + ); + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 1, { + returned: 7, + }), + AssertionError, + "spy call did not resolve to expected value", + ); + await assertRejects( + () => + assertSpyCallAsync(spyMethod, 1, { + returned: Promise.resolve(7), + }), + AssertionError, + "spy call did not resolve to expected value", + ); + await assertRejects( + () => assertSpyCallAsync(spyMethod, 2), + AssertionError, + "spy not called as much as expected", + ); +}); + +Deno.test("assertSpyCallAync on sync value", async () => { + const spyFunc = spy(() => 4 as unknown as Promise); + + spyFunc(); + await assertRejects( + () => assertSpyCallAsync(spyFunc, 0), + AssertionError, + "spy call did not return a promise, a value was returned.", + ); +}); + +Deno.test("assertSpyCallAync on sync error", async () => { + const spyFunc = spy(() => { + throw new ExampleError("failed"); + }); + + await assertRejects(() => spyFunc(), ExampleError, "fail"); + await assertRejects( + () => assertSpyCallAsync(spyFunc, 0), + AssertionError, + "spy call did not return a promise, an error was thrown.", + ); +}); + +Deno.test("assertSpyCallAync error", async () => { + const spyFunc = spy((..._args: number[]): Promise => + Promise.reject(new ExampleError("failed")) + ); + + await assertRejects(() => spyFunc(), ExampleError, "fail"); + await assertSpyCallAsync(spyFunc, 0); + await assertSpyCallAsync(spyFunc, 0, { + args: [], + self: undefined, + error: { + Class: ExampleError, + msgIncludes: "fail", + }, + }); + await assertSpyCallAsync(spyFunc, 0, { + args: [], + }); + await assertSpyCallAsync(spyFunc, 0, { + self: undefined, + }); + await assertSpyCallAsync(spyFunc, 0, { + error: { + Class: ExampleError, + msgIncludes: "fail", + }, + }); + await assertSpyCallAsync(spyFunc, 0, { + error: { + Class: Error, + msgIncludes: "fail", + }, + }); + + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + args: [1], + self: {}, + error: { + Class: OtherError, + msgIncludes: "fail", + }, + }), + AssertionError, + "spy not called with expected args", + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + args: [1], + }), + AssertionError, + "spy not called with expected args", + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + self: {}, + }), + AssertionError, + "spy not called as method on expected self", + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + error: { + Class: OtherError, + msgIncludes: "fail", + }, + }), + AssertionError, + 'Expected error to be instance of "OtherError"', + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + error: { + Class: OtherError, + msgIncludes: "x", + }, + }), + AssertionError, + 'Expected error to be instance of "OtherError"', + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + error: { + Class: ExampleError, + msgIncludes: "x", + }, + }), + AssertionError, + 'Expected error message to include "x", but got "failed".', + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + error: { + Class: Error, + msgIncludes: "x", + }, + }), + AssertionError, + 'Expected error message to include "x", but got "failed".', + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + error: { + msgIncludes: "x", + }, + }), + AssertionError, + 'Expected error message to include "x", but got "failed".', + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + returned: Promise.resolve(7), + }), + AssertionError, + "spy call returned promise was rejected", + ); + await assertRejects( + () => + assertSpyCallAsync(spyFunc, 0, { + returned: Promise.resolve(7), + error: { msgIncludes: "x" }, + }), + TypeError, + "do not expect error and return, only one should be expected", + ); + await assertRejects( + () => assertSpyCallAsync(spyFunc, 1), + AssertionError, + "spy not called as much as expected", + ); +}); + +Deno.test("assertSpyArg", () => { + const spyFunc = spy(); + + assertThrows( + () => assertSpyCallArg(spyFunc, 0, 0, undefined), + AssertionError, + "spy not called as much as expected", + ); + + spyFunc(); + assertSpyCallArg(spyFunc, 0, 0, undefined); + assertSpyCallArg(spyFunc, 0, 1, undefined); + assertThrows( + () => assertSpyCallArg(spyFunc, 0, 0, 2), + AssertionError, + "Values are not equal:", + ); + + spyFunc(7, 9); + assertSpyCallArg(spyFunc, 1, 0, 7); + assertSpyCallArg(spyFunc, 1, 1, 9); + assertSpyCallArg(spyFunc, 1, 2, undefined); + assertThrows( + () => assertSpyCallArg(spyFunc, 0, 0, 9), + AssertionError, + "Values are not equal:", + ); + assertThrows( + () => assertSpyCallArg(spyFunc, 0, 1, 7), + AssertionError, + "Values are not equal:", + ); + assertThrows( + () => assertSpyCallArg(spyFunc, 0, 2, 7), + AssertionError, + "Values are not equal:", + ); +}); + +Deno.test("assertSpyArgs without range", () => { + const spyFunc = spy(); + + assertThrows( + () => assertSpyCallArgs(spyFunc, 0, []), + AssertionError, + "spy not called as much as expected", + ); + + spyFunc(); + assertSpyCallArgs(spyFunc, 0, []); + assertThrows( + () => assertSpyCallArgs(spyFunc, 0, [undefined]), + AssertionError, + "Values are not equal:", + ); + assertThrows( + () => assertSpyCallArgs(spyFunc, 0, [2]), + AssertionError, + "Values are not equal:", + ); + + spyFunc(7, 9); + assertSpyCallArgs(spyFunc, 1, [7, 9]); + assertThrows( + () => assertSpyCallArgs(spyFunc, 1, [7, 9, undefined]), + AssertionError, + "Values are not equal:", + ); + assertThrows( + () => assertSpyCallArgs(spyFunc, 1, [9, 7]), + AssertionError, + "Values are not equal:", + ); +}); + +Deno.test("assertSpyArgs with start only", () => { + const spyFunc = spy(); + + assertThrows( + () => assertSpyCallArgs(spyFunc, 0, 1, []), + AssertionError, + "spy not called as much as expected", + ); + + spyFunc(); + assertSpyCallArgs(spyFunc, 0, 1, []); + assertThrows( + () => assertSpyCallArgs(spyFunc, 0, 1, [undefined]), + AssertionError, + "Values are not equal:", + ); + assertThrows( + () => assertSpyCallArgs(spyFunc, 0, 1, [2]), + AssertionError, + "Values are not equal:", + ); + + spyFunc(7, 9, 8); + assertSpyCallArgs(spyFunc, 1, 1, [9, 8]); + assertThrows( + () => assertSpyCallArgs(spyFunc, 1, 1, [9, 8, undefined]), + AssertionError, + "Values are not equal:", + ); + assertThrows( + () => assertSpyCallArgs(spyFunc, 1, 1, [9, 7]), + AssertionError, + "Values are not equal:", + ); +}); + +Deno.test("assertSpyArgs with range", () => { + const spyFunc = spy(); + + assertThrows( + () => assertSpyCallArgs(spyFunc, 0, 1, 3, []), + AssertionError, + "spy not called as much as expected", + ); + + spyFunc(); + assertSpyCallArgs(spyFunc, 0, 1, 3, []); + assertThrows( + () => assertSpyCallArgs(spyFunc, 0, 1, 3, [undefined, undefined]), + AssertionError, + "Values are not equal:", + ); + assertThrows( + () => assertSpyCallArgs(spyFunc, 0, 1, 3, [2, 4]), + AssertionError, + "Values are not equal:", + ); + + spyFunc(7, 9, 8, 5, 6); + assertSpyCallArgs(spyFunc, 1, 1, 3, [9, 8]); + assertThrows( + () => assertSpyCallArgs(spyFunc, 1, 1, 3, [9, 8, undefined]), + AssertionError, + "Values are not equal:", + ); + assertThrows( + () => assertSpyCallArgs(spyFunc, 1, 1, 3, [9, 7]), + AssertionError, + "Values are not equal:", + ); +}); + +Deno.test("returnsThis", () => { + const callback = returnsThis(); + const obj = { callback, x: 1, y: 2 }; + const obj2 = { x: 2, y: 3 }; + assertEquals(callback(), undefined); + assertEquals(obj.callback(), obj); + assertEquals(callback.apply(obj2, []), obj2); +}); + +Deno.test("returnsArg", () => { + let callback = returnsArg(0); + assertEquals(callback(), undefined); + assertEquals(callback("a"), "a"); + assertEquals(callback("b", "c"), "b"); + callback = returnsArg(1); + assertEquals(callback(), undefined); + assertEquals(callback("a"), undefined); + assertEquals(callback("b", "c"), "c"); + assertEquals(callback("d", "e", "f"), "e"); +}); + +Deno.test("returnsArgs", () => { + let callback = returnsArgs(); + assertEquals(callback(), []); + assertEquals(callback("a"), ["a"]); + assertEquals(callback("b", "c"), ["b", "c"]); + callback = returnsArgs(1); + assertEquals(callback(), []); + assertEquals(callback("a"), []); + assertEquals(callback("b", "c"), ["c"]); + assertEquals(callback("d", "e", "f"), ["e", "f"]); + callback = returnsArgs(1, 3); + assertEquals(callback("a"), []); + assertEquals(callback("b", "c"), ["c"]); + assertEquals(callback("d", "e", "f"), ["e", "f"]); + assertEquals(callback("d", "e", "f", "g"), ["e", "f"]); +}); + +Deno.test("returnsNext with array", () => { + let results = [1, 2, new Error("oops"), 3]; + let callback = returnsNext(results); + assertEquals(callback(), 1); + assertEquals(callback(), 2); + assertThrows(() => callback(), Error, "oops"); + assertEquals(callback(), 3); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 4 times", + ); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 4 times", + ); + + results = []; + callback = returnsNext(results); + results.push(1, 2, new Error("oops"), 3); + assertEquals(callback(), 1); + assertEquals(callback(), 2); + assertThrows(() => callback(), Error, "oops"); + assertEquals(callback(), 3); + results.push(4); + assertEquals(callback(), 4); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 5 times", + ); + results.push(5); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 5 times", + ); +}); + +Deno.test("returnsNext with iterator", () => { + let results = [1, 2, new Error("oops"), 3]; + let callback = returnsNext(results.values()); + assertEquals(callback(), 1); + assertEquals(callback(), 2); + assertThrows(() => callback(), Error, "oops"); + assertEquals(callback(), 3); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 4 times", + ); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 4 times", + ); + + results = []; + callback = returnsNext(results.values()); + results.push(1, 2, new Error("oops"), 3); + assertEquals(callback(), 1); + assertEquals(callback(), 2); + assertThrows(() => callback(), Error, "oops"); + assertEquals(callback(), 3); + results.push(4); + assertEquals(callback(), 4); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 5 times", + ); + results.push(5); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 5 times", + ); +}); + +Deno.test("returnsNext with generator", () => { + let results = [1, 2, new Error("oops"), 3]; + const generator = function* () { + yield* results; + }; + let callback = returnsNext(generator()); + assertEquals(callback(), 1); + assertEquals(callback(), 2); + assertThrows(() => callback(), Error, "oops"); + assertEquals(callback(), 3); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 4 times", + ); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 4 times", + ); + + results = []; + callback = returnsNext(generator()); + results.push(1, 2, new Error("oops"), 3); + assertEquals(callback(), 1); + assertEquals(callback(), 2); + assertThrows(() => callback(), Error, "oops"); + assertEquals(callback(), 3); + results.push(4); + assertEquals(callback(), 4); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 5 times", + ); + results.push(5); + assertThrows( + () => callback(), + MockError, + "not expected to be called more than 5 times", + ); +}); + +Deno.test("resolvesNext with array", async () => { + let results = [ + 1, + new Error("oops"), + Promise.resolve(2), + Promise.resolve(new Error("oops")), + 3, + ]; + let callback = resolvesNext(results); + const value = callback(); + assertEquals(Promise.resolve(value), value); + assertEquals(await value, 1); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 2); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 3); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 5 times", + ); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 5 times", + ); + + results = []; + callback = resolvesNext(results); + results.push( + 1, + new Error("oops"), + Promise.resolve(2), + Promise.resolve(new Error("oops")), + 3, + ); + assertEquals(await callback(), 1); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 2); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 3); + results.push(4); + assertEquals(await callback(), 4); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 6 times", + ); + results.push(5); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 6 times", + ); +}); + +Deno.test("resolvesNext with iterator", async () => { + let results = [ + 1, + new Error("oops"), + Promise.resolve(2), + Promise.resolve(new Error("oops")), + 3, + ]; + let callback = resolvesNext(results.values()); + const value = callback(); + assertEquals(Promise.resolve(value), value); + assertEquals(await value, 1); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 2); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 3); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 5 times", + ); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 5 times", + ); + + results = []; + callback = resolvesNext(results.values()); + results.push( + 1, + new Error("oops"), + Promise.resolve(2), + Promise.resolve(new Error("oops")), + 3, + ); + assertEquals(await callback(), 1); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 2); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 3); + results.push(4); + assertEquals(await callback(), 4); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 6 times", + ); + results.push(5); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 6 times", + ); +}); + +Deno.test("resolvesNext with async generator", async () => { + let results = [ + 1, + new Error("oops"), + Promise.resolve(2), + Promise.resolve(new Error("oops")), + 3, + ]; + const asyncGenerator = async function* () { + await delay(0); + yield* results; + }; + let callback = resolvesNext(asyncGenerator()); + const value = callback(); + assertEquals(Promise.resolve(value), value); + assertEquals(await value, 1); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 2); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 3); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 5 times", + ); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 5 times", + ); + + results = []; + callback = resolvesNext(asyncGenerator()); + results.push( + 1, + new Error("oops"), + Promise.resolve(2), + Promise.resolve(new Error("oops")), + 3, + ); + assertEquals(await callback(), 1); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 2); + assertRejects(() => callback(), Error, "oops"); + assertEquals(await callback(), 3); + results.push(4); + assertEquals(await callback(), 4); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 6 times", + ); + results.push(5); + assertRejects( + async () => await callback(), + MockError, + "not expected to be called more than 6 times", + ); +});