From 4380bbc53f158ca61f84f4cf819ec1f8621e5b47 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 20 Mar 2022 17:45:59 -0500 Subject: [PATCH 1/6] feat(testing): Add mocking utilities --- testing/mock/_asserts.ts | 324 +++++++++++ testing/mock/_asserts_test.ts | 932 ++++++++++++++++++++++++++++++++ testing/mock/_callbacks.ts | 93 ++++ testing/mock/_callbacks_test.ts | 343 ++++++++++++ testing/mock/_test_utils.ts | 18 + testing/mock/mock.ts | 431 +++++++++++++++ testing/mock/mock_test.ts | 513 ++++++++++++++++++ testing/mock/mod.ts | 3 + 8 files changed, 2657 insertions(+) create mode 100644 testing/mock/_asserts.ts create mode 100644 testing/mock/_asserts_test.ts create mode 100644 testing/mock/_callbacks.ts create mode 100644 testing/mock/_callbacks_test.ts create mode 100755 testing/mock/_test_utils.ts create mode 100644 testing/mock/mock.ts create mode 100644 testing/mock/mock_test.ts create mode 100644 testing/mock/mod.ts diff --git a/testing/mock/_asserts.ts b/testing/mock/_asserts.ts new file mode 100644 index 000000000000..8666ffa39f2a --- /dev/null +++ b/testing/mock/_asserts.ts @@ -0,0 +1,324 @@ +/** This module is browser compatible. */ + +import { + assertEquals, + AssertionError, + assertIsError, + assertRejects, +} from "../asserts.ts"; +import { Spy, SpyCall } from "./mock.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"; + } +} + +/** + * 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. + * Returns the call. + */ +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, + ); + } + } + return call; +} + +/** + * Asserts that an async spy is called as expected. + * Returns the call. + */ +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; + } + const call: SpyCall = assertSpyCall( + spy, + callIndex, + expectedSync, + ); + + 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 ?? "", + ); + } + } + return call; +} + +/** + * Asserts that a spy is called with a specific arg as expected. + * Returns the actual arg. + */ +export function assertSpyCallArg< + Self, + Args extends unknown[], + Return, + ExpectedArg, +>( + spy: Spy, + callIndex: number, + argIndex: number, + expected: ExpectedArg, +): ExpectedArg { + const call: SpyCall = assertSpyCall(spy, 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. + * Returns the actual args. + */ +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 { + const call: SpyCall = assertSpyCall(spy, 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; +} diff --git a/testing/mock/_asserts_test.ts b/testing/mock/_asserts_test.ts new file mode 100644 index 000000000000..3008a413d29b --- /dev/null +++ b/testing/mock/_asserts_test.ts @@ -0,0 +1,932 @@ +import { AssertionError, assertRejects, assertThrows } from "../asserts.ts"; +import { + assertSpyCall, + assertSpyCallArg, + assertSpyCallArgs, + assertSpyCallAsync, + assertSpyCalls, +} from "./_asserts.ts"; +import { Point } from "./_test_utils.ts"; +import { spy, stub } from "./mock.ts"; + +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:", + ); +}); diff --git a/testing/mock/_callbacks.ts b/testing/mock/_callbacks.ts new file mode 100644 index 000000000000..2149b5f695c4 --- /dev/null +++ b/testing/mock/_callbacks.ts @@ -0,0 +1,93 @@ +/** This module is browser compatible. */ + +import { MockError } from "./mock.ts"; + +/** 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/_callbacks_test.ts b/testing/mock/_callbacks_test.ts new file mode 100644 index 000000000000..963e9c55b9d1 --- /dev/null +++ b/testing/mock/_callbacks_test.ts @@ -0,0 +1,343 @@ +import { assertEquals, assertRejects, assertThrows } from "../asserts.ts"; +import { delay } from "../../async/delay.ts"; +import { + resolvesNext, + returnsArg, + returnsArgs, + returnsNext, + returnsThis, +} from "./_callbacks.ts"; +import { MockError } from "./mock.ts"; + +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", + ); +}); diff --git a/testing/mock/_test_utils.ts b/testing/mock/_test_utils.ts new file mode 100755 index 000000000000..9f8b2e7d39a5 --- /dev/null +++ b/testing/mock/_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/mock.ts b/testing/mock/mock.ts new file mode 100644 index 000000000000..3ca5e971fb6d --- /dev/null +++ b/testing/mock/mock.ts @@ -0,0 +1,431 @@ +/** This module is browser compatible. */ + +import { MockError } from "./_asserts.ts"; + +export * from "./_asserts.ts"; +export * from "./_callbacks.ts"; + +/** 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; +} diff --git a/testing/mock/mock_test.ts b/testing/mock/mock_test.ts new file mode 100644 index 000000000000..e10da960bc59 --- /dev/null +++ b/testing/mock/mock_test.ts @@ -0,0 +1,513 @@ +import { assertEquals, assertNotEquals, assertThrows } from "../asserts.ts"; +import { + assertSpyCall, + assertSpyCalls, + MockError, + mockSession, + mockSessionAsync, + restore, + 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(); + } +}); diff --git a/testing/mock/mod.ts b/testing/mock/mod.ts new file mode 100644 index 000000000000..8096719c5f17 --- /dev/null +++ b/testing/mock/mod.ts @@ -0,0 +1,3 @@ +/** This module is browser compatible. */ + +export * from "./mock.ts"; From a35179af7a6edfd68cdf05964624eae7dacf8cb2 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sat, 26 Mar 2022 09:59:51 -0500 Subject: [PATCH 2/6] Make assertion functions not return anything --- testing/mock/_asserts.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/testing/mock/_asserts.ts b/testing/mock/_asserts.ts index 8666ffa39f2a..d2b7baed8ecd 100644 --- a/testing/mock/_asserts.ts +++ b/testing/mock/_asserts.ts @@ -68,7 +68,6 @@ export interface ExpectedSpyCall< /** * Asserts that a spy is called as expected. - * Returns the call. */ export function assertSpyCall< Self, @@ -144,12 +143,10 @@ export function assertSpyCall< ); } } - return call; } /** * Asserts that an async spy is called as expected. - * Returns the call. */ export async function assertSpyCallAsync< Self, @@ -165,11 +162,8 @@ export async function assertSpyCallAsync< delete expectedSync.returned; delete expectedSync.error; } - const call: SpyCall = assertSpyCall( - spy, - callIndex, - expectedSync, - ); + assertSpyCall(spy, callIndex, expectedSync); + const call = spy.calls[callIndex]; if (call.error) { throw new AssertionError( @@ -229,7 +223,6 @@ export async function assertSpyCallAsync< ); } } - return call; } /** @@ -247,7 +240,8 @@ export function assertSpyCallArg< argIndex: number, expected: ExpectedArg, ): ExpectedArg { - const call: SpyCall = assertSpyCall(spy, callIndex); + assertSpyCall(spy, callIndex); + const call = spy.calls[callIndex]; const arg = call.args[argIndex]; assertEquals(arg, expected); return arg as ExpectedArg; @@ -258,7 +252,6 @@ export function assertSpyCallArg< * 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. - * Returns the actual args. */ export function assertSpyCallArgs< Self, @@ -305,7 +298,8 @@ export function assertSpyCallArgs< argsEnd?: number | ExpectedArgs, expected?: ExpectedArgs, ): ExpectedArgs { - const call: SpyCall = assertSpyCall(spy, callIndex); + assertSpyCall(spy, callIndex); + const call = spy.calls[callIndex]; if (!expected) { expected = argsEnd as ExpectedArgs; argsEnd = undefined; From 7246c771db182a8c209db25d817531ad493b731f Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sat, 26 Mar 2022 10:02:53 -0500 Subject: [PATCH 3/6] Fix description for assert function --- testing/mock/_asserts.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/mock/_asserts.ts b/testing/mock/_asserts.ts index d2b7baed8ecd..ecf41a2fd2be 100644 --- a/testing/mock/_asserts.ts +++ b/testing/mock/_asserts.ts @@ -227,7 +227,6 @@ export async function assertSpyCallAsync< /** * Asserts that a spy is called with a specific arg as expected. - * Returns the actual arg. */ export function assertSpyCallArg< Self, From 7386899fd77d44ee9324cb53854e5a821c0659db Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 27 Mar 2022 10:35:55 -0500 Subject: [PATCH 4/6] Move mock files out of mock directory --- testing/{mock => }/_asserts.ts | 2 +- testing/{mock => }/_asserts_test.ts | 2 +- testing/{mock => }/_callbacks.ts | 0 testing/{mock => }/_callbacks_test.ts | 4 ++-- testing/{mock => }/_test_utils.ts | 0 testing/{mock => }/mock.ts | 0 testing/mock/mod.ts | 3 --- testing/{mock => }/mock_test.ts | 2 +- 8 files changed, 5 insertions(+), 8 deletions(-) rename testing/{mock => }/_asserts.ts (99%) rename testing/{mock => }/_asserts_test.ts (99%) rename testing/{mock => }/_callbacks.ts (100%) rename testing/{mock => }/_callbacks_test.ts (98%) rename testing/{mock => }/_test_utils.ts (100%) rename testing/{mock => }/mock.ts (100%) delete mode 100644 testing/mock/mod.ts rename testing/{mock => }/mock_test.ts (99%) diff --git a/testing/mock/_asserts.ts b/testing/_asserts.ts similarity index 99% rename from testing/mock/_asserts.ts rename to testing/_asserts.ts index ecf41a2fd2be..109bbb893d03 100644 --- a/testing/mock/_asserts.ts +++ b/testing/_asserts.ts @@ -5,7 +5,7 @@ import { AssertionError, assertIsError, assertRejects, -} from "../asserts.ts"; +} from "./asserts.ts"; import { Spy, SpyCall } from "./mock.ts"; /** An error related to spying on a function or instance method. */ diff --git a/testing/mock/_asserts_test.ts b/testing/_asserts_test.ts similarity index 99% rename from testing/mock/_asserts_test.ts rename to testing/_asserts_test.ts index 3008a413d29b..bd11be868b1a 100644 --- a/testing/mock/_asserts_test.ts +++ b/testing/_asserts_test.ts @@ -1,4 +1,4 @@ -import { AssertionError, assertRejects, assertThrows } from "../asserts.ts"; +import { AssertionError, assertRejects, assertThrows } from "./asserts.ts"; import { assertSpyCall, assertSpyCallArg, diff --git a/testing/mock/_callbacks.ts b/testing/_callbacks.ts similarity index 100% rename from testing/mock/_callbacks.ts rename to testing/_callbacks.ts diff --git a/testing/mock/_callbacks_test.ts b/testing/_callbacks_test.ts similarity index 98% rename from testing/mock/_callbacks_test.ts rename to testing/_callbacks_test.ts index 963e9c55b9d1..7e3190cfdbb6 100644 --- a/testing/mock/_callbacks_test.ts +++ b/testing/_callbacks_test.ts @@ -1,5 +1,5 @@ -import { assertEquals, assertRejects, assertThrows } from "../asserts.ts"; -import { delay } from "../../async/delay.ts"; +import { assertEquals, assertRejects, assertThrows } from "./asserts.ts"; +import { delay } from "../async/delay.ts"; import { resolvesNext, returnsArg, diff --git a/testing/mock/_test_utils.ts b/testing/_test_utils.ts similarity index 100% rename from testing/mock/_test_utils.ts rename to testing/_test_utils.ts diff --git a/testing/mock/mock.ts b/testing/mock.ts similarity index 100% rename from testing/mock/mock.ts rename to testing/mock.ts diff --git a/testing/mock/mod.ts b/testing/mock/mod.ts deleted file mode 100644 index 8096719c5f17..000000000000 --- a/testing/mock/mod.ts +++ /dev/null @@ -1,3 +0,0 @@ -/** This module is browser compatible. */ - -export * from "./mock.ts"; diff --git a/testing/mock/mock_test.ts b/testing/mock_test.ts similarity index 99% rename from testing/mock/mock_test.ts rename to testing/mock_test.ts index e10da960bc59..caa34b2c0bb9 100644 --- a/testing/mock/mock_test.ts +++ b/testing/mock_test.ts @@ -1,4 +1,4 @@ -import { assertEquals, assertNotEquals, assertThrows } from "../asserts.ts"; +import { assertEquals, assertNotEquals, assertThrows } from "./asserts.ts"; import { assertSpyCall, assertSpyCalls, From e8f0b94a48a114c649496947f674785889fab4c0 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 27 Mar 2022 11:37:16 -0500 Subject: [PATCH 5/6] Move _asserts.ts and _callbacks.ts into mock.ts --- testing/_asserts.ts | 317 --------- testing/_asserts_test.ts | 932 -------------------------- testing/_callbacks.ts | 93 --- testing/_callbacks_test.ts | 343 ---------- testing/mock.ts | 406 +++++++++++- testing/mock_test.ts | 1272 +++++++++++++++++++++++++++++++++++- 6 files changed, 1674 insertions(+), 1689 deletions(-) delete mode 100644 testing/_asserts.ts delete mode 100644 testing/_asserts_test.ts delete mode 100644 testing/_callbacks.ts delete mode 100644 testing/_callbacks_test.ts diff --git a/testing/_asserts.ts b/testing/_asserts.ts deleted file mode 100644 index 109bbb893d03..000000000000 --- a/testing/_asserts.ts +++ /dev/null @@ -1,317 +0,0 @@ -/** This module is browser compatible. */ - -import { - assertEquals, - AssertionError, - assertIsError, - assertRejects, -} from "./asserts.ts"; -import { Spy, SpyCall } from "./mock.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"; - } -} - -/** - * 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; -} diff --git a/testing/_asserts_test.ts b/testing/_asserts_test.ts deleted file mode 100644 index bd11be868b1a..000000000000 --- a/testing/_asserts_test.ts +++ /dev/null @@ -1,932 +0,0 @@ -import { AssertionError, assertRejects, assertThrows } from "./asserts.ts"; -import { - assertSpyCall, - assertSpyCallArg, - assertSpyCallArgs, - assertSpyCallAsync, - assertSpyCalls, -} from "./_asserts.ts"; -import { Point } from "./_test_utils.ts"; -import { spy, stub } from "./mock.ts"; - -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:", - ); -}); diff --git a/testing/_callbacks.ts b/testing/_callbacks.ts deleted file mode 100644 index 2149b5f695c4..000000000000 --- a/testing/_callbacks.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** This module is browser compatible. */ - -import { MockError } from "./mock.ts"; - -/** 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/_callbacks_test.ts b/testing/_callbacks_test.ts deleted file mode 100644 index 7e3190cfdbb6..000000000000 --- a/testing/_callbacks_test.ts +++ /dev/null @@ -1,343 +0,0 @@ -import { assertEquals, assertRejects, assertThrows } from "./asserts.ts"; -import { delay } from "../async/delay.ts"; -import { - resolvesNext, - returnsArg, - returnsArgs, - returnsNext, - returnsThis, -} from "./_callbacks.ts"; -import { MockError } from "./mock.ts"; - -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", - ); -}); diff --git a/testing/mock.ts b/testing/mock.ts index 3ca5e971fb6d..75e8f0656750 100644 --- a/testing/mock.ts +++ b/testing/mock.ts @@ -1,9 +1,19 @@ /** This module is browser compatible. */ -import { MockError } from "./_asserts.ts"; +import { + assertEquals, + AssertionError, + assertIsError, + assertRejects, +} from "./asserts.ts"; -export * from "./_asserts.ts"; -export * from "./_callbacks.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< @@ -429,3 +439,393 @@ export function 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_test.ts b/testing/mock_test.ts index caa34b2c0bb9..7cf1f6ef0131 100644 --- a/testing/mock_test.ts +++ b/testing/mock_test.ts @@ -1,11 +1,26 @@ -import { assertEquals, assertNotEquals, assertThrows } from "./asserts.ts"; +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, @@ -511,3 +526,1258 @@ Deno.test("mockSession and restore multiple sessions", () => { 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", + ); +}); From 39c17fa3a40381634aee5ad9df1d5bb542c09e17 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 27 Mar 2022 15:21:54 -0500 Subject: [PATCH 6/6] Add mock section to README.md with examples --- testing/README.md | 211 ++++++++++++++++++ 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 +++ 7 files changed, 309 insertions(+) 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 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/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); +});