From 4380bbc53f158ca61f84f4cf819ec1f8621e5b47 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 20 Mar 2022 17:45:59 -0500 Subject: [PATCH 01/16] 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 02/16] 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 03/16] 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 2d5e8d8efa21c546dbe5d000fb7f36cd1ca5c5d0 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sat, 26 Mar 2022 19:21:55 -0500 Subject: [PATCH 04/16] feat(testing): Add behavior-driven development --- testing/README.md | 296 ++++ testing/_test_suite.ts | 348 +++++ testing/bdd.ts | 407 ++++++ testing/bdd_examples/user.ts | 22 + testing/bdd_examples/user_flat_test.ts | 43 + testing/bdd_examples/user_mixed_test.ts | 42 + testing/bdd_examples/user_nested_test.ts | 42 + testing/bdd_examples/user_test.ts | 31 + testing/bdd_test.ts | 1623 ++++++++++++++++++++++ 9 files changed, 2854 insertions(+) create mode 100644 testing/_test_suite.ts create mode 100644 testing/bdd.ts create mode 100644 testing/bdd_examples/user.ts create mode 100644 testing/bdd_examples/user_flat_test.ts create mode 100644 testing/bdd_examples/user_mixed_test.ts create mode 100644 testing/bdd_examples/user_nested_test.ts create mode 100644 testing/bdd_examples/user_test.ts create mode 100644 testing/bdd_test.ts diff --git a/testing/README.md b/testing/README.md index 932eb15c1bf6..b7ebbcc6d010 100644 --- a/testing/README.md +++ b/testing/README.md @@ -277,3 +277,299 @@ 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. + +## Behavior-driven development + +With the `bdd.ts` module you can write your tests in a familiar format for +grouping tests and adding setup/teardown hooks used by other JavaScript testing +frameworks like Jasmine, Jest, and Mocha. + +The `describe` function creates a block that groups together several related +tests. The `it` function registers an individual test case. The `describe` and +`it` functions have similar call signatures to `Deno.test`, making it easy to +migrate from using `Deno.test`. + +### Hooks + +There are 4 types of hooks available for test suites. A test suite can have +multiples of each type of hook, they will be called in the order that they are +registered. The `afterEach` and `afterAll` hooks will be called whether or not +the test case passes. The all hooks will be called once for the whole group +while the each hooks will be called for each individual test case. + +- `beforeAll`: Runs before all of the tests in the test suite. +- `afterAll`: Runs after all of the tests in the test suite finish. +- `beforeEach`: Runs before each of the individual test cases in the test suite. +- `afterEach`: Runs after each of the individual test cases in the test suite. + +If a hook is registered at the top level, a global test suite will be registered +and all tests will belong to it. Hooks registered at the top level must be +registered before any individual test cases or test suites. + +### Focusing tests + +If you would like to only run specific individual test cases, you can do so by +calling `it.only` instead of `it`. If you would like to only run specific test +suites, you can do so by calling `describe.only` instead of `describe`. + +There is one limitation to this when the individual test cases or test suites +belong to another test suite, they will be the only ones to run within the top +level test suite. + +### Ignoring tests + +If you would like to not run specific individual test cases, you can do so by +calling `it.ignore` instead of `it`. If you would like to only run specific test +suites, you can do so by calling `describe.ignore` instead of `describe`. + +### Sanitization options + +Like `Deno.TestDefinition`, the `DescribeDefinition` and `ItDefinition` have +sanitization options. They work in the same way. + +- sanitizeExit: Ensure the test case does not prematurely cause the process to + exit, for example via a call to Deno.exit. Defaults to true. +- sanitizeOps: Check that the number of async completed ops after the test is + the same as number of dispatched ops. Defaults to true. +- sanitizeResources: Ensure the test case does not "leak" resources - ie. the + resource table after the test has exactly the same contents as before the + test. Defaults to true. + +### Permissions option + +Like `Deno.TestDefinition`, the `DescribeDefintion` and `ItDefinition` have a +permissions option. They specify the permissions that should be used to run an +individual test case or test suite. Set this to "inherit" to keep the calling +thread's permissions. Set this to "none" to revoke all permissions. + +Defaults to "inherit". + +There is currently one limitation to this, you cannot use the permissions option +on an individual test case or test suite that belongs to another test suite. + +### Migrating and usage + +To migrate from `Deno.test`, all you have to do is replace `Deno.test` with +`it`. If you are using the step API, you will need to replace `Deno.test` with +describe and steps with `describe` or `it`. The callback for individual test +cases can be syncronous or asyncronous. + +Below is an example of a test file using `Deno.test` and `t.step`. In the +following sections there are examples of how it can be converted to using nested +test grouping, flat test grouping, and a mix of both. + +```ts +import { + assertEquals, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts"; + +Deno.test("User.users initially empty", () => { + assertEquals(User.users.size, 0); +}); + +Deno.test("User constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } +}); + +Deno.test("User age", async (t) => { + const user = new User("Kyle"); + + await t.step("getAge", () => { + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + await t.step("setAge", () => { + user.setAge(18); + assertEquals(user.getAge(), 18); + }); +}); +``` + +#### Nested test grouping + +Tests created within the callback of a `describe` function call will belong to +the new test suite it creates. The hooks can be created within it or be added to +the options argument for describe. + +```ts +import { + assertEquals, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { + afterEach, + beforeEach, + describe, + it, +} from "https://deno.land/std@$STD_VERSION/testing/bdd.ts"; +import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts"; + +describe("User", () => { + it("users initially empty", () => { + assertEquals(User.users.size, 0); + }); + + it("constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } + }); + + describe("age", () => { + let user: User; + + beforeEach(() => { + user = new User("Kyle"); + }); + + afterEach(() => { + User.users.clear(); + }); + + it("getAge", function () { + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + it("setAge", function () { + user.setAge(18); + assertEquals(user.getAge(), 18); + }); + }); +}); +``` + +#### Flat test grouping + +The `describe` function returns a unique symbol that can be used to reference +the test suite for adding tests to it without having to create them within a +callback. The gives you the ability to have test grouping without any extra +indentation in front of the grouped tests. + +```ts +import { + assertEquals, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { + describe, + it, +} from "https://deno.land/std@$STD_VERSION/testing/bdd.ts"; +import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts"; + +const userTests = describe("User"); + +it(userTests, "users initially empty", () => { + assertEquals(User.users.size, 0); +}); + +it(userTests, "constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } +}); + +const ageTests = describe({ + name: "age", + suite: userTests, + beforeEach(this: { user: User }) { + this.user = new User("Kyle"); + }, + afterEach() { + User.users.clear(); + }, +}); + +it(ageTests, "getAge", function () { + const { user } = this; + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); +}); + +it(ageTests, "setAge", function () { + const { user } = this; + user.setAge(18); + assertEquals(user.getAge(), 18); +}); +``` + +#### Mixed test grouping + +Both nested test grouping and flat test grouping can be used together. This can +be useful if you'd like to create deep groupings without all the extra +indentation in front of each line. + +```ts +import { + assertEquals, + assertStrictEquals, + assertThrows, +} from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; +import { + describe, + it, +} from "https://deno.land/std@$STD_VERSION/testing/bdd.ts"; +import { User } from "https://deno.land/std@$STD_VERSION/testing/bdd_examples/user.ts"; + +describe("User", () => { + it("users initially empty", () => { + assertEquals(User.users.size, 0); + }); + + it("constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } + }); + + const ageTests = describe({ + name: "age", + beforeEach(this: { user: User }) { + this.user = new User("Kyle"); + }, + afterEach() { + User.users.clear(); + }, + }); + + it(ageTests, "getAge", function () { + const { user } = this; + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + it(ageTests, "setAge", function () { + const { user } = this; + user.setAge(18); + assertEquals(user.getAge(), 18); + }); +}); +``` diff --git a/testing/_test_suite.ts b/testing/_test_suite.ts new file mode 100644 index 000000000000..5d2d2a1fba92 --- /dev/null +++ b/testing/_test_suite.ts @@ -0,0 +1,348 @@ +/** The options for creating a test suite with the describe function. */ +export interface DescribeDefinition extends Omit { + fn?: () => void; + /** + * The `describe` function returns a `TestSuite` representing the group of tests. + * If `describe` is called within another `describe` calls `fn`, the suite will default to that parent `describe` calls returned `TestSuite`. + * If `describe` is not called within another `describe` calls `fn`, the suite will default to the `TestSuite` representing the global group of tests. + */ + suite?: TestSuite; + /** Run some shared setup before all of the tests in the suite. */ + beforeAll?: + | ((this: T) => void | Promise) + | ((this: T) => void | Promise)[]; + /** Run some shared teardown after all of the tests in the suite. */ + afterAll?: + | ((this: T) => void | Promise) + | ((this: T) => void | Promise)[]; + /** Run some shared setup before each test in the suite. */ + beforeEach?: + | ((this: T) => void | Promise) + | ((this: T) => void | Promise)[]; + /** Run some shared teardown after each test in the suite. */ + afterEach?: + | ((this: T) => void | Promise) + | ((this: T) => void | Promise)[]; +} + +/** The options for creating an individual test case with the it function. */ +export interface ItDefinition extends Omit { + fn: (this: T) => void | Promise; + /** + * The `describe` function returns a `TestSuite` representing the group of tests. + * If `it` is called within a `describe` calls `fn`, the suite will default to that parent `describe` calls returned `TestSuite`. + * If `it` is not called within a `describe` calls `fn`, the suite will default to the `TestSuite` representing the global group of tests. + */ + suite?: TestSuite; +} + +/** The names of all the different types of hooks. */ +export type HookNames = "beforeAll" | "afterAll" | "beforeEach" | "afterEach"; + +/** Optional test definition keys. */ +const optionalTestDefinitionKeys: (keyof Deno.TestDefinition)[] = [ + "only", + "permissions", + "ignore", + "sanitizeExit", + "sanitizeOps", + "sanitizeResources", +]; + +/** Optional test step definition keys. */ +const optionalTestStepDefinitionKeys: (keyof Deno.TestStepDefinition)[] = [ + "ignore", + "sanitizeExit", + "sanitizeOps", + "sanitizeResources", +]; + +/** + * A group of tests. + */ +export interface TestSuite { + symbol: symbol; +} + +/** + * An internal representation of a group of tests. + */ +export class TestSuiteInternal implements TestSuite { + symbol: symbol; + protected describe: DescribeDefinition; + protected steps: (TestSuiteInternal | ItDefinition)[]; + protected hasOnlyStep: boolean; + + constructor(describe: DescribeDefinition) { + this.describe = describe; + this.steps = []; + this.hasOnlyStep = false; + + const { suite } = describe; + if (suite && !TestSuiteInternal.suites.has(suite.symbol)) { + throw new Error("suite does not represent a registered test suite"); + } + const testSuite = suite + ? TestSuiteInternal.suites.get(suite.symbol) + : TestSuiteInternal.current; + this.symbol = Symbol(); + TestSuiteInternal.suites.set(this.symbol, this); + + const { fn } = describe; + if (fn) { + const temp = TestSuiteInternal.current; + TestSuiteInternal.current = this; + try { + fn(); + } finally { + TestSuiteInternal.current = temp; + } + } + + if (testSuite) { + TestSuiteInternal.addStep(testSuite, this); + } else { + const { + name, + ignore, + only, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + } = describe; + TestSuiteInternal.registerTest({ + name, + ignore, + only, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + fn: async (t) => { + if (!TestSuiteInternal.running) TestSuiteInternal.running = true; + const context = {} as T; + const { beforeAll } = this.describe; + if (typeof beforeAll === "function") { + await beforeAll.call(context); + } else if (beforeAll) { + for (const hook of beforeAll) { + await hook.call(context); + } + } + try { + TestSuiteInternal.active.push(this.symbol); + await TestSuiteInternal.run(this, context, t); + } finally { + TestSuiteInternal.active.pop(); + const { afterAll } = this.describe; + if (typeof afterAll === "function") { + await afterAll.call(context); + } else if (afterAll) { + for (const hook of afterAll) { + await hook.call(context); + } + } + } + }, + }); + } + } + + /** If the test cases have begun executing. */ + static running = false; + + /** If a test has been registered yet. Block adding global hooks if a test has been registered. */ + static started = false; + + /** A map of all test suites by symbol. */ + // deno-lint-ignore no-explicit-any + static suites = new Map>(); + + /** The current test suite being registered. */ + // deno-lint-ignore no-explicit-any + static current: TestSuiteInternal | null = null; + + /** The stack of tests that are actively running. */ + static active: symbol[] = []; + + /** This is used internally for testing this module. */ + static reset(): void { + TestSuiteInternal.running = false; + TestSuiteInternal.started = false; + TestSuiteInternal.current = null; + TestSuiteInternal.active = []; + } + + /** This is used internally to register tests. */ + static registerTest(options: Deno.TestDefinition): void { + options = { ...options }; + optionalTestDefinitionKeys.forEach((key) => { + if (typeof options[key] === "undefined") delete options[key]; + }); + Deno.test(options); + } + + /** Updates all steps within top level suite to have ignore set to true if only is not set to true on step. */ + static addingOnlyStep(suite: TestSuiteInternal) { + if (!suite.hasOnlyStep) { + for (let i = 0; i < suite.steps.length; i++) { + const step = suite.steps[i]!; + if (step instanceof TestSuiteInternal) { + if (!(step.hasOnlyStep || step.describe.only)) { + suite.steps.splice(i--, 1); + } + } else { + if (!step.only) { + suite.steps.splice(i--, 1); + } + } + } + suite.hasOnlyStep = true; + } + } + + /** This is used internally to add steps to a test suite. */ + static addStep( + suite: TestSuiteInternal, + step: TestSuiteInternal | ItDefinition, + ): void { + if (!suite.hasOnlyStep) { + if (step instanceof TestSuiteInternal) { + if (step.hasOnlyStep || step.describe.only) { + TestSuiteInternal.addingOnlyStep(suite); + } + } else { + if (step.only) TestSuiteInternal.addingOnlyStep(suite); + } + } + + let omit = false; + if (suite.hasOnlyStep) { + if (step instanceof TestSuiteInternal) { + if (!(step.hasOnlyStep || step.describe.only)) omit = true; + } else { + if (!step.only) omit = true; + } + } + + if (!omit) suite.steps.push(step); + } + + /** This is used internally to add hooks to a test suite. */ + static setHook( + suite: TestSuiteInternal, + name: HookNames, + fn: (this: T) => void | Promise, + ): void { + if (suite.describe[name]) { + if (typeof suite.describe[name] === "function") { + suite.describe[name] = [ + suite.describe[name] as ((this: T) => void | Promise), + ]; + } + (suite.describe[name] as ((this: T) => void | Promise)[]).push(fn); + } else { + suite.describe[name] = fn; + } + } + + /** This is used internally to run all steps for a test suite. */ + static async run( + suite: TestSuiteInternal, + context: T, + t: Deno.TestContext, + ): Promise { + for (const step of suite.steps) { + const { + name, + fn, + ignore, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + } = step instanceof TestSuiteInternal ? step.describe : step; + + const options: Deno.TestStepDefinition = { + name, + ignore, + sanitizeExit, + sanitizeOps, + sanitizeResources, + fn: async (t) => { + if (permissions) { + throw new Error( + "permissions option not available for nested tests", + ); + } + context = { ...context }; + if (step instanceof TestSuiteInternal) { + const { beforeAll } = step.describe; + if (typeof beforeAll === "function") { + await beforeAll.call(context); + } else if (beforeAll) { + for (const hook of beforeAll) { + await hook.call(context); + } + } + try { + TestSuiteInternal.active.push(step.symbol); + await TestSuiteInternal.run(step, context, t); + } finally { + TestSuiteInternal.active.pop(); + const { afterAll } = step.describe; + if (typeof afterAll === "function") { + await afterAll.call(context); + } else if (afterAll) { + for (const hook of afterAll) { + await hook.call(context); + } + } + } + } else { + await TestSuiteInternal.runTest(fn!, context); + } + }, + }; + optionalTestStepDefinitionKeys.forEach((key) => { + if (typeof options[key] === "undefined") delete options[key]; + }); + await t.step(options); + } + } + + static async runTest( + fn: (this: T) => void | Promise, + context: T, + activeIndex = 0, + ) { + const suite = TestSuiteInternal.active[activeIndex]; + const testSuite = suite && TestSuiteInternal.suites.get(suite); + if (testSuite) { + context = { ...context }; + const { beforeEach } = testSuite.describe; + if (typeof beforeEach === "function") { + await beforeEach.call(context); + } else if (beforeEach) { + for (const hook of beforeEach) { + await hook.call(context); + } + } + try { + await TestSuiteInternal.runTest(fn, context, activeIndex + 1); + } finally { + const { afterEach } = testSuite.describe; + if (typeof afterEach === "function") { + await afterEach.call(context); + } else if (afterEach) { + for (const hook of afterEach) { + await hook.call(context); + } + } + } + } else { + await fn.call(context); + } + } +} diff --git a/testing/bdd.ts b/testing/bdd.ts new file mode 100644 index 000000000000..142a02fd341c --- /dev/null +++ b/testing/bdd.ts @@ -0,0 +1,407 @@ +import { + DescribeDefinition, + HookNames, + ItDefinition, + TestSuite, + TestSuiteInternal, +} from "./_test_suite.ts"; +export type { DescribeDefinition, ItDefinition, TestSuite }; + +/** The arguments for an ItFunction. */ +export type ItArgs = + | [options: ItDefinition] + | [ + name: string, + options: Omit, "name">, + ] + | [ + name: string, + fn: (this: T) => void | Promise, + ] + | [fn: (this: T) => void | Promise] + | [ + name: string, + options: Omit, "fn" | "name">, + fn: (this: T) => void | Promise, + ] + | [ + options: Omit, "fn">, + fn: (this: T) => void | Promise, + ] + | [ + options: Omit, "fn" | "name">, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + name: string, + options: Omit, "name" | "suite">, + ] + | [ + suite: TestSuite, + name: string, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + name: string, + options: Omit, "fn" | "name" | "suite">, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + options: Omit, "fn" | "suite">, + fn: (this: T) => void | Promise, + ] + | [ + suite: TestSuite, + options: Omit, "fn" | "name" | "suite">, + fn: (this: T) => void | Promise, + ]; + +/** Generates an ItDefinition from ItArgs. */ +function itDefinition(...args: ItArgs): ItDefinition { + let [ + suiteOptionsOrNameOrFn, + optionsOrNameOrFn, + optionsOrFn, + fn, + ] = args; + let suite: TestSuite | undefined = undefined; + let name: string; + let options: + | ItDefinition + | Omit, "fn"> + | Omit, "name"> + | Omit, "fn" | "name">; + if ( + typeof suiteOptionsOrNameOrFn === "object" && + typeof (suiteOptionsOrNameOrFn as TestSuite).symbol === "symbol" + ) { + suite = suiteOptionsOrNameOrFn as TestSuite; + } else { + fn = optionsOrFn as typeof fn; + optionsOrFn = optionsOrNameOrFn as typeof optionsOrFn; + optionsOrNameOrFn = suiteOptionsOrNameOrFn as typeof optionsOrNameOrFn; + } + if (typeof optionsOrNameOrFn === "string") { + name = optionsOrNameOrFn; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + options = {}; + } else { + options = optionsOrFn!; + if (!fn) fn = (options as Omit, "name">).fn; + } + } else if (typeof optionsOrNameOrFn === "function") { + fn = optionsOrNameOrFn; + name = fn.name; + options = {}; + } else { + options = optionsOrNameOrFn!; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + } else { + fn = (options as ItDefinition).fn; + } + name = (options as ItDefinition).name ?? fn.name; + } + + return { + suite, + ...options, + name, + fn, + }; +} + +/** Registers an individual test case. */ +export interface it { + (...args: ItArgs): void; + + /** Registers an individual test case with only set to true. */ + only(...args: ItArgs): void; + + /** Registers an individual test case with ignore set to true. */ + ignore(...args: ItArgs): void; +} + +/** Registers an individual test case. */ +export function it(...args: ItArgs): void { + if (TestSuiteInternal.running) { + throw new Error( + "cannot register new test cases after already registered test cases start running", + ); + } + const options = itDefinition(...args); + const { suite } = options; + const testSuite = suite + ? TestSuiteInternal.suites.get(suite.symbol) + : TestSuiteInternal.current; + + if (!TestSuiteInternal.started) TestSuiteInternal.started = true; + if (testSuite) { + TestSuiteInternal.addStep(testSuite, options); + } else { + const { + name, + fn, + ignore, + only, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + } = options; + TestSuiteInternal.registerTest({ + name, + ignore, + only, + permissions, + sanitizeExit, + sanitizeOps, + sanitizeResources, + async fn() { + if (!TestSuiteInternal.running) TestSuiteInternal.running = true; + await fn.call({} as T); + }, + }); + } +} + +it.only = function itOnly(...args: ItArgs): void { + const options = itDefinition(...args); + return it({ + ...options, + only: true, + }); +}; + +it.ignore = function itIgnore(...args: ItArgs): void { + const options = itDefinition(...args); + return it({ + ...options, + ignore: true, + }); +}; + +function addHook( + name: HookNames, + fn: (this: T) => void | Promise, +): void { + if (!TestSuiteInternal.current) { + if (TestSuiteInternal.started) { + throw new Error( + "cannot add global hooks after a global test is registered", + ); + } + TestSuiteInternal.current = new TestSuiteInternal({ + name: "global", + [name]: fn, + }); + } else { + TestSuiteInternal.setHook(TestSuiteInternal.current!, name, fn); + } +} + +/** Run some shared setup before all of the tests in the suite. */ +export function beforeAll( + fn: (this: T) => void | Promise, +): void { + addHook("beforeAll", fn); +} + +/** Run some shared teardown after all of the tests in the suite. */ +export function afterAll( + fn: (this: T) => void | Promise, +): void { + addHook("afterAll", fn); +} + +/** Run some shared setup before each test in the suite. */ +export function beforeEach( + fn: (this: T) => void | Promise, +): void { + addHook("beforeEach", fn); +} + +/** Run some shared teardown after each test in the suite. */ +export function afterEach( + fn: (this: T) => void | Promise, +): void { + addHook("afterEach", fn); +} + +/** The arguments for a DescribeFunction. */ +export type DescribeArgs = + | [options: DescribeDefinition] + | [name: string] + | [ + name: string, + options: Omit, "name">, + ] + | [name: string, fn: () => void] + | [fn: () => void] + | [ + name: string, + options: Omit, "fn" | "name">, + fn: () => void, + ] + | [ + options: Omit, "fn">, + fn: () => void, + ] + | [ + options: Omit, "fn" | "name">, + fn: () => void, + ] + | [ + suite: TestSuite, + name: string, + ] + | [ + suite: TestSuite, + name: string, + options: Omit, "name" | "suite">, + ] + | [ + suite: TestSuite, + name: string, + fn: () => void, + ] + | [ + suite: TestSuite, + fn: () => void, + ] + | [ + suite: TestSuite, + name: string, + options: Omit, "fn" | "name" | "suite">, + fn: () => void, + ] + | [ + suite: TestSuite, + options: Omit, "fn" | "suite">, + fn: () => void, + ] + | [ + suite: TestSuite, + options: Omit, "fn" | "name" | "suite">, + fn: () => void, + ]; + +/** Generates a DescribeDefinition from DescribeArgs. */ +function describeDefinition( + ...args: DescribeArgs +): DescribeDefinition { + let [ + suiteOptionsOrNameOrFn, + optionsOrNameOrFn, + optionsOrFn, + fn, + ] = args; + let suite: TestSuite | undefined = undefined; + let name: string; + let options: + | DescribeDefinition + | Omit, "fn"> + | Omit, "name"> + | Omit, "fn" | "name">; + if ( + typeof suiteOptionsOrNameOrFn === "object" && + typeof (suiteOptionsOrNameOrFn as TestSuite).symbol === "symbol" + ) { + suite = suiteOptionsOrNameOrFn as TestSuite; + } else { + fn = optionsOrFn as typeof fn; + optionsOrFn = optionsOrNameOrFn as typeof optionsOrFn; + optionsOrNameOrFn = suiteOptionsOrNameOrFn as typeof optionsOrNameOrFn; + } + if (typeof optionsOrNameOrFn === "string") { + name = optionsOrNameOrFn; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + options = {}; + } else { + options = optionsOrFn ?? {}; + if (!fn) fn = (options as Omit, "name">).fn; + } + } else if (typeof optionsOrNameOrFn === "function") { + fn = optionsOrNameOrFn; + name = fn.name; + options = {}; + } else { + options = optionsOrNameOrFn ?? {}; + if (typeof optionsOrFn === "function") { + fn = optionsOrFn; + } else { + fn = (options as DescribeDefinition).fn; + } + name = (options as DescribeDefinition).name ?? fn?.name ?? ""; + } + + if (!suite) { + suite = options.suite; + } + if (!suite && TestSuiteInternal.current) { + const { symbol } = TestSuiteInternal.current; + suite = { symbol }; + } + + return { + ...options, + suite, + name, + fn, + }; +} + +/** Registers a test suite. */ +export interface describe { + (...args: DescribeArgs): TestSuite; + + /** Registers a test suite with only set to true. */ + only(...args: DescribeArgs): TestSuite; + + /** Registers a test suite with ignore set to true. */ + ignore(...args: DescribeArgs): TestSuite; +} + +/** Registers a test suite. */ +export function describe( + ...args: DescribeArgs +): TestSuite { + if (TestSuiteInternal.running) { + throw new Error( + "cannot register new test suites after already registered test cases start running", + ); + } + const options = describeDefinition(...args); + if (!TestSuiteInternal.started) TestSuiteInternal.started = true; + const { symbol } = new TestSuiteInternal(options); + return { symbol }; +} + +describe.only = function describeOnly( + ...args: DescribeArgs +): TestSuite { + const options = describeDefinition(...args); + return describe({ + ...options, + only: true, + }); +}; + +describe.ignore = function describeIgnore( + ...args: DescribeArgs +): TestSuite { + const options = describeDefinition(...args); + return describe({ + ...options, + ignore: true, + }); +}; diff --git a/testing/bdd_examples/user.ts b/testing/bdd_examples/user.ts new file mode 100644 index 000000000000..ae1ce2091a21 --- /dev/null +++ b/testing/bdd_examples/user.ts @@ -0,0 +1,22 @@ +export class User { + static users: Map = new Map(); + age?: number; + + constructor(public name: string) { + if (User.users.has(name)) { + throw new Error(`User ${name} already exists`); + } + User.users.set(name, this); + } + + getAge(): number { + if (!this.age) { + throw new Error("Age unknown"); + } + return this.age; + } + + setAge(age: number) { + this.age = age; + } +} diff --git a/testing/bdd_examples/user_flat_test.ts b/testing/bdd_examples/user_flat_test.ts new file mode 100644 index 000000000000..61bd7faf1770 --- /dev/null +++ b/testing/bdd_examples/user_flat_test.ts @@ -0,0 +1,43 @@ +import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; +import { describe, it } from "../bdd.ts"; +import { User } from "./user.ts"; + +const userTests = describe("User"); + +it(userTests, "users initially empty", () => { + assertEquals(User.users.size, 0); +}); + +it(userTests, "constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } +}); + +const ageTests = describe({ + name: "age", + suite: userTests, + beforeEach(this: { user: User }) { + this.user = new User("Kyle"); + }, + afterEach() { + User.users.clear(); + }, +}); + +it(ageTests, "getAge", function () { + const { user } = this; + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); +}); + +it(ageTests, "setAge", function () { + const { user } = this; + user.setAge(18); + assertEquals(user.getAge(), 18); +}); diff --git a/testing/bdd_examples/user_mixed_test.ts b/testing/bdd_examples/user_mixed_test.ts new file mode 100644 index 000000000000..ea2a21df8ac8 --- /dev/null +++ b/testing/bdd_examples/user_mixed_test.ts @@ -0,0 +1,42 @@ +import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; +import { describe, it } from "../bdd.ts"; +import { User } from "./user.ts"; + +describe("User", () => { + it("users initially empty", () => { + assertEquals(User.users.size, 0); + }); + + it("constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } + }); + + const ageTests = describe({ + name: "age", + beforeEach(this: { user: User }) { + this.user = new User("Kyle"); + }, + afterEach() { + User.users.clear(); + }, + }); + + it(ageTests, "getAge", function () { + const { user } = this; + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + it(ageTests, "setAge", function () { + const { user } = this; + user.setAge(18); + assertEquals(user.getAge(), 18); + }); +}); diff --git a/testing/bdd_examples/user_nested_test.ts b/testing/bdd_examples/user_nested_test.ts new file mode 100644 index 000000000000..4463fe02a5c4 --- /dev/null +++ b/testing/bdd_examples/user_nested_test.ts @@ -0,0 +1,42 @@ +import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; +import { afterEach, beforeEach, describe, it } from "../bdd.ts"; +import { User } from "./user.ts"; + +describe("User", () => { + it("users initially empty", () => { + assertEquals(User.users.size, 0); + }); + + it("constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } + }); + + describe("age", () => { + let user: User; + + beforeEach(() => { + user = new User("Kyle"); + }); + + afterEach(() => { + User.users.clear(); + }); + + it("getAge", function () { + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + it("setAge", function () { + user.setAge(18); + assertEquals(user.getAge(), 18); + }); + }); +}); diff --git a/testing/bdd_examples/user_test.ts b/testing/bdd_examples/user_test.ts new file mode 100644 index 000000000000..dffd31727344 --- /dev/null +++ b/testing/bdd_examples/user_test.ts @@ -0,0 +1,31 @@ +import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; +import { User } from "./user.ts"; + +Deno.test("User.users initially empty", () => { + assertEquals(User.users.size, 0); +}); + +Deno.test("User constructor", () => { + try { + const user = new User("Kyle"); + assertEquals(user.name, "Kyle"); + assertStrictEquals(User.users.get("Kyle"), user); + } finally { + User.users.clear(); + } +}); + +Deno.test("User age", async (t) => { + const user = new User("Kyle"); + + await t.step("getAge", () => { + assertThrows(() => user.getAge(), Error, "Age unknown"); + user.age = 18; + assertEquals(user.getAge(), 18); + }); + + await t.step("setAge", () => { + user.setAge(18); + assertEquals(user.getAge(), 18); + }); +}); diff --git a/testing/bdd_test.ts b/testing/bdd_test.ts new file mode 100644 index 000000000000..617aae0f2b58 --- /dev/null +++ b/testing/bdd_test.ts @@ -0,0 +1,1623 @@ +import { + assert, + assertEquals, + assertObjectMatch, + assertStrictEquals, +} from "./asserts.ts"; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + it, +} from "./bdd.ts"; +import { TestSuiteInternal } from "./_test_suite.ts"; +import { assertSpyCall, assertSpyCalls, Spy, spy, stub } from "./mock/mod.ts"; + +Deno.test("global", async (t) => { + class TestContext implements Deno.TestContext { + steps: TestContext[]; + spies: { + step: Spy; + }; + + constructor() { + this.spies = { + step: spy(this, "step"), + }; + this.steps = []; + } + + async step(t: Deno.TestStepDefinition): Promise; + async step( + name: string, + fn: (t: Deno.TestContext) => void | Promise, + ): Promise; + async step( + tOrName: Deno.TestStepDefinition | string, + fn?: (t: Deno.TestContext) => void | Promise, + ): Promise { + let ignore = false; + if (typeof tOrName === "object") { + ignore = tOrName.ignore ?? false; + fn = tOrName.fn; + } + + const context = new TestContext(); + this.steps.push(context); + if (!ignore) { + await fn!(context); + } + return !ignore; + } + } + + const baseStepOptions: Omit = { + ignore: false, + sanitizeExit: true, + sanitizeOps: true, + sanitizeResources: true, + }; + + const baseOptions: Omit = { + ...baseStepOptions, + only: false, + permissions: "inherit", + }; + + interface GlobalContext { + allTimer: number; + eachTimer: number; + } + + let timerIdx = 1; + const timers = new Map(); + function hookFns() { + timerIdx = 1; + timers.clear(); + return { + beforeAllFn: spy(async function (this: GlobalContext) { + await Promise.resolve(); + this.allTimer = timerIdx++; + timers.set(this.allTimer, setTimeout(() => {}, 10000)); + }), + afterAllFn: spy(async function (this: GlobalContext) { + await Promise.resolve(); + clearTimeout(timers.get(this.allTimer)); + }), + beforeEachFn: spy(async function (this: GlobalContext) { + await Promise.resolve(); + this.eachTimer = timerIdx++; + timers.set(this.eachTimer, setTimeout(() => {}, 10000)); + }), + afterEachFn: spy(async function (this: GlobalContext) { + await Promise.resolve(); + clearTimeout(timers.get(this.eachTimer)); + }), + }; + } + + await t.step("global hooks", async () => { + const test = stub(Deno, "test"), + fns = [spy(), spy()], + { beforeAllFn, afterAllFn, beforeEachFn, afterEachFn } = hookFns(); + + try { + beforeAll(beforeAllFn); + afterAll(afterAllFn); + + beforeEach(beforeEachFn); + afterEach(afterEachFn); + + assertEquals(it({ name: "example 1", fn: fns[0] }), undefined); + assertEquals(it({ name: "example 2", fn: fns[1] }), undefined); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals(Object.keys(options).sort(), ["fn", "name"]); + assertEquals(options.name, "global"); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 2 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 3 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + assertSpyCalls(beforeAllFn, 1); + assertSpyCalls(afterAllFn, 1); + assertSpyCalls(beforeEachFn, 2); + assertSpyCalls(afterEachFn, 2); + }); + + await t.step("it", async (t) => { + async function assertOptions( + expectedOptions: Omit, + cb: (fn: Spy) => void, + ): Promise { + const test = stub(Deno, "test"); + const fn = spy(); + try { + cb(fn); + + assertSpyCalls(fn, 0); + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals( + Object.keys(options).sort(), + ["name", "fn", ...Object.keys(expectedOptions)].sort(), + ); + assertObjectMatch(options, { + name: "example", + ...expectedOptions, + }); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 0); + assertSpyCall(fn, 0, { + self: {}, + args: [], + returned: undefined, + }); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + } + + async function assertMinimumOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({}, cb); + } + + async function assertAllOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions(baseOptions, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it({ name: "example", fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it({ + name: "example", + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step("signature 2", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it("example", { fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it("example", { + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step("signature 3", async () => + await assertMinimumOptions((fn) => { + assertEquals(it("example", fn), undefined); + })); + + await t.step("signature 4", async () => + await assertMinimumOptions((fn) => { + assertEquals( + it(function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + })); + + await t.step("signature 5", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it("example", {}, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it("example", { + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it({ name: "example" }, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it({ + name: "example", + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it({}, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it({ + ...baseOptions, + }, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + })); + }); + + await t.step("only", async (t) => { + async function assertMinimumOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({ only: true }, cb); + } + + async function assertAllOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({ ...baseOptions, only: true }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only({ name: "example", fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only({ + name: "example", + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step("signature 2", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only("example", { fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only("example", { + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step( + "signature 3", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only("example", fn), undefined); + }), + ); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it.only(function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("signature 5", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only("example", {}, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only("example", { + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.only({ name: "example" }, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only({ + name: "example", + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it.only({}, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.only({ + ...baseOptions, + }, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + })); + }); + }); + + await t.step("ignore", async (t) => { + async function assertMinimumOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({ ignore: true }, cb); + } + + async function assertAllOptions( + cb: (fn: Spy) => void, + ): Promise { + await assertOptions({ ...baseOptions, ignore: true }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore({ name: "example", fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore({ + name: "example", + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step("signature 2", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore("example", { fn }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore("example", { + fn, + ...baseOptions, + }), + undefined, + ); + })); + }); + + await t.step( + "signature 3", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore("example", fn), undefined); + }), + ); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it.ignore(function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("signature 5", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore("example", {}, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore("example", { + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals(it.ignore({ name: "example" }, fn), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore({ + name: "example", + ...baseOptions, + }, fn), + undefined, + ); + })); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fn) => { + assertEquals( + it.ignore({}, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fn) => { + assertEquals( + it.ignore({ + ...baseOptions, + }, function example(this: void, ...args) { + fn.apply(this, args); + }), + undefined, + ); + })); + }); + }); + }); + + await t.step("describe", async (t) => { + async function assertOptions( + expectedOptions: Omit, + cb: (fns: Spy[]) => void, + ): Promise { + const test = stub(Deno, "test"); + const fns = [spy(), spy()]; + try { + cb(fns); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals( + Object.keys(options).sort(), + ["name", "fn", ...Object.keys(expectedOptions)].sort(), + ); + assertObjectMatch(options, { + name: "example", + ...expectedOptions, + }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: {}, + args: [], + returned: undefined, + }); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: {}, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + } + + async function assertMinimumOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({}, cb); + } + + async function assertAllOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ ...baseOptions }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe({ name: "example" }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe({ + name: "example", + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step( + "signature 2", + async () => + await assertMinimumOptions((fns) => { + const suite = describe("example"); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 3", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe("example", {}); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe("example", { + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fns) => { + const suite = describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "signature 5", + async () => + await assertMinimumOptions((fns) => { + const suite = describe(function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe("example", {}, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe("example", { + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe({ name: "example" }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe({ + name: "example", + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step("signature 8", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe({}, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("all options", async () => + await assertAllOptions((fns) => { + const suite = describe({ + ...baseOptions, + }, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + })); + }); + + await t.step("only", async (t) => { + async function assertMinimumOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ only: true }, cb); + } + + async function assertAllOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ ...baseOptions, only: true }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only({ name: "example" }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only({ + name: "example", + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step( + "signature 2", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only("example"); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 3", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only("example", {}); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only("example", { + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "signature 5", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only(function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only("example", {}, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only("example", { + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only({ name: "example" }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only({ + name: "example", + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step("signature 8", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.only({}, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.only({ + ...baseOptions, + }, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + }); + + await t.step("ignore", async (t) => { + async function assertMinimumOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ ignore: true }, cb); + } + + async function assertAllOptions( + cb: (fns: Spy[]) => void, + ): Promise { + await assertOptions({ ...baseOptions, ignore: true }, cb); + } + + await t.step("signature 1", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore({ name: "example" }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore({ + name: "example", + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step( + "signature 2", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore("example"); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 3", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore("example", {}); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "a", fn: fns[0] }), undefined); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore("example", { + fn: () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }, + ...baseOptions, + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step( + "signature 4", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "signature 5", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore(function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step("signature 6", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore("example", {}, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore("example", { + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step("signature 7", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore({ name: "example" }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore({ + name: "example", + ...baseOptions, + }, () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + + await t.step("signature 8", async (t) => { + await t.step( + "minimum options", + async () => + await assertMinimumOptions((fns) => { + const suite = describe.ignore({}, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + + await t.step( + "all options", + async () => + await assertAllOptions((fns) => { + const suite = describe.ignore({ + ...baseOptions, + }, function example() { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + }); + assert(suite && typeof suite.symbol === "symbol"); + assertEquals(it({ suite, name: "b", fn: fns[1] }), undefined); + }), + ); + }); + }); + + await t.step("nested only", async (t) => { + async function assertOnly( + cb: (fns: Spy[]) => void, + ): Promise { + const test = stub(Deno, "test"); + const fns = [spy(), spy(), spy()]; + try { + cb(fns); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals( + Object.keys(options).sort(), + ["name", "fn"].sort(), + ); + assertObjectMatch(options, { + name: "example", + }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 1); + + let fn = fns[0]; + assertSpyCalls(fn, 0); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: {}, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[2]; + assertSpyCalls(fn, 0); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + } + + await t.step("it", async () => + await assertOnly((fns) => { + describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + assertEquals(it.only({ name: "b", fn: fns[1] }), undefined); + assertEquals(it({ name: "c", fn: fns[2] }), undefined); + }); + })); + + await t.step("nested it", async () => + await assertOnly((fns) => { + describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + describe("nested", () => { + assertEquals(it.only({ name: "b", fn: fns[1] }), undefined); + }); + assertEquals(it({ name: "c", fn: fns[2] }), undefined); + }); + })); + + await t.step("describe", async () => + await assertOnly((fns) => { + describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + describe.only("nested", () => { + assertEquals(it({ name: "b", fn: fns[1] }), undefined); + }); + assertEquals(it({ name: "c", fn: fns[2] }), undefined); + }); + })); + + await t.step("nested describe", async () => + await assertOnly((fns) => { + describe("example", () => { + assertEquals(it({ name: "a", fn: fns[0] }), undefined); + describe("nested", () => { + describe.only("nested 2", () => { + assertEquals(it({ name: "b", fn: fns[1] }), undefined); + }); + }); + assertEquals(it({ name: "c", fn: fns[2] }), undefined); + }); + })); + }); + + await t.step("with hooks", async (t) => { + async function assertHooks( + cb: ( + options: { + beforeAllFn: Spy; + afterAllFn: Spy; + beforeEachFn: Spy; + afterEachFn: Spy; + fns: Spy[]; + }, + ) => void, + ) { + const test = stub(Deno, "test"), + fns = [spy(), spy()], + { beforeAllFn, afterAllFn, beforeEachFn, afterEachFn } = hookFns(); + + try { + cb({ beforeAllFn, afterAllFn, beforeEachFn, afterEachFn, fns }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals(Object.keys(options).sort(), ["fn", "name"]); + assertEquals(options.name, "example"); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 2 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 3 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + assertSpyCalls(beforeAllFn, 1); + assertSpyCalls(afterAllFn, 1); + assertSpyCalls(beforeEachFn, 2); + assertSpyCalls(afterEachFn, 2); + } + + await t.step( + "in callback", + async () => + await assertHooks( + ({ beforeAllFn, afterAllFn, beforeEachFn, afterEachFn, fns }) => { + describe("example", () => { + beforeAll(beforeAllFn); + afterAll(afterAllFn); + + beforeEach(beforeEachFn); + afterEach(afterEachFn); + + assertEquals(it({ name: "example 1", fn: fns[0] }), undefined); + assertEquals(it({ name: "example 2", fn: fns[1] }), undefined); + }); + }, + ), + ); + + await t.step( + "in options", + async () => + await assertHooks( + ({ beforeAllFn, afterAllFn, beforeEachFn, afterEachFn, fns }) => { + describe({ + name: "example", + beforeAll: beforeAllFn, + afterAll: afterAllFn, + beforeEach: beforeEachFn, + afterEach: afterEachFn, + fn: () => { + assertEquals( + it({ name: "example 1", fn: fns[0] }), + undefined, + ); + assertEquals( + it({ name: "example 2", fn: fns[1] }), + undefined, + ); + }, + }); + }, + ), + ); + + await t.step( + "nested", + async () => { + const test = stub(Deno, "test"), + fns = [spy(), spy()], + { beforeAllFn, afterAllFn, beforeEachFn, afterEachFn } = hookFns(); + + try { + describe("example", () => { + beforeAll(beforeAllFn); + afterAll(afterAllFn); + + beforeEach(beforeEachFn); + afterEach(afterEachFn); + + describe("nested", () => { + assertEquals(it({ name: "example 1", fn: fns[0] }), undefined); + assertEquals(it({ name: "example 2", fn: fns[1] }), undefined); + }); + }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals(Object.keys(options).sort(), ["fn", "name"]); + assertEquals(options.name, "example"); + + let context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 1); + + context = context.steps[0]; + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 2 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: { allTimer: 1, eachTimer: 3 }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + assertSpyCalls(beforeAllFn, 1); + assertSpyCalls(afterAllFn, 1); + assertSpyCalls(beforeEachFn, 2); + assertSpyCalls(afterEachFn, 2); + }, + ); + + interface NestedContext extends GlobalContext { + allTimerNested: number; + eachTimerNested: number; + } + + await t.step( + "nested with hooks", + async () => { + const test = stub(Deno, "test"), + fns = [spy(), spy()], + { beforeAllFn, afterAllFn, beforeEachFn, afterEachFn } = hookFns(), + beforeAllFnNested = spy(async function (this: NestedContext) { + await Promise.resolve(); + this.allTimerNested = timerIdx++; + timers.set( + this.allTimerNested, + setTimeout(() => {}, 10000), + ); + }), + afterAllFnNested = spy( + async function (this: NestedContext) { + await Promise.resolve(); + clearTimeout(timers.get(this.allTimerNested)); + }, + ), + beforeEachFnNested = spy(async function (this: NestedContext) { + await Promise.resolve(); + this.eachTimerNested = timerIdx++; + timers.set( + this.eachTimerNested, + setTimeout(() => {}, 10000), + ); + }), + afterEachFnNested = spy( + async function (this: NestedContext) { + await Promise.resolve(); + clearTimeout(timers.get(this.eachTimerNested)); + }, + ); + + try { + describe("example", () => { + beforeAll(beforeAllFn); + afterAll(afterAllFn); + + beforeEach(beforeEachFn); + afterEach(afterEachFn); + + describe("nested", () => { + beforeAll(beforeAllFnNested); + afterAll(afterAllFnNested); + + beforeEach(beforeEachFnNested); + afterEach(afterEachFnNested); + + assertEquals(it({ name: "example 1", fn: fns[0] }), undefined); + assertEquals(it({ name: "example 2", fn: fns[1] }), undefined); + }); + }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals(Object.keys(options).sort(), ["fn", "name"]); + assertEquals(options.name, "example"); + + let context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 1); + + context = context.steps[0]; + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 2); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + + let fn = fns[0]; + assertSpyCall(fn, 0, { + self: { + allTimer: 1, + allTimerNested: 2, + eachTimer: 3, + eachTimerNested: 4, + }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: { + allTimer: 1, + allTimerNested: 2, + eachTimer: 5, + eachTimerNested: 6, + }, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + assertSpyCalls(beforeAllFn, 1); + assertSpyCalls(afterAllFn, 1); + assertSpyCalls(beforeEachFn, 2); + assertSpyCalls(afterEachFn, 2); + + assertSpyCalls(beforeAllFnNested, 1); + assertSpyCalls(afterAllFnNested, 1); + assertSpyCalls(beforeEachFnNested, 2); + assertSpyCalls(afterEachFnNested, 2); + }, + ); + }); + }); +}); From 7386899fd77d44ee9324cb53854e5a821c0659db Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 27 Mar 2022 10:35:55 -0500 Subject: [PATCH 05/16] 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 06/16] 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 3cf64222396472ae5305879a7456c32f1651ff55 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 27 Mar 2022 11:41:08 -0500 Subject: [PATCH 07/16] Fix mock import --- testing/bdd_test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/bdd_test.ts b/testing/bdd_test.ts index 617aae0f2b58..9c1a671a48e0 100644 --- a/testing/bdd_test.ts +++ b/testing/bdd_test.ts @@ -13,7 +13,7 @@ import { it, } from "./bdd.ts"; import { TestSuiteInternal } from "./_test_suite.ts"; -import { assertSpyCall, assertSpyCalls, Spy, spy, stub } from "./mock/mod.ts"; +import { assertSpyCall, assertSpyCalls, Spy, spy, stub } from "./mock.ts"; Deno.test("global", async (t) => { class TestContext implements Deno.TestContext { From 39c17fa3a40381634aee5ad9df1d5bb542c09e17 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 27 Mar 2022 15:21:54 -0500 Subject: [PATCH 08/16] 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); +}); From 9313cb493ac0a2145435d2dcc41cb9421e21c221 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 27 Mar 2022 15:59:56 -0500 Subject: [PATCH 09/16] Add comments linking to examples shown --- testing/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/testing/README.md b/testing/README.md index 34c496903b8c..4a75c1cde788 100644 --- a/testing/README.md +++ b/testing/README.md @@ -359,6 +359,7 @@ following sections there are examples of how it can be converted to using nested test grouping, flat test grouping, and a mix of both. ```ts +// https://deno.land/std@$STD_VERSION/testing/bdd_examples/user_test.ts import { assertEquals, assertStrictEquals, @@ -403,6 +404,7 @@ the new test suite it creates. The hooks can be created within it or be added to the options argument for describe. ```ts +// https://deno.land/std@$STD_VERSION/testing/bdd_examples/user_nested_test.ts import { assertEquals, assertStrictEquals, @@ -464,6 +466,7 @@ callback. The gives you the ability to have test grouping without any extra indentation in front of the grouped tests. ```ts +// https://deno.land/std@$STD_VERSION/testing/bdd_examples/user_flat_test.ts import { assertEquals, assertStrictEquals, @@ -523,6 +526,7 @@ be useful if you'd like to create deep groupings without all the extra indentation in front of each line. ```ts +// https://deno.land/std@$STD_VERSION/testing/bdd_examples/user_mixed_test.ts import { assertEquals, assertStrictEquals, From eb0b1ab8edb7b9985330c07a469d7257e097b1bb Mon Sep 17 00:00:00 2001 From: Kyle June Date: Tue, 29 Mar 2022 20:01:29 -0500 Subject: [PATCH 10/16] Fix typo in mocking section --- testing/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/README.md b/testing/README.md index 4a75c1cde788..706c40e3ccf7 100644 --- a/testing/README.md +++ b/testing/README.md @@ -712,8 +712,8 @@ second example. 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 +way to spy on the `randomInt` function. That could be done with 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. From d01d75d651364219ed9b2e469cf396a5939fb1da Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 3 Apr 2022 14:59:28 -0500 Subject: [PATCH 11/16] Fix nested only option only applying to current suite --- testing/_test_suite.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/testing/_test_suite.ts b/testing/_test_suite.ts index 5d2d2a1fba92..07f174be13a3 100644 --- a/testing/_test_suite.ts +++ b/testing/_test_suite.ts @@ -200,6 +200,13 @@ export class TestSuiteInternal implements TestSuite { } suite.hasOnlyStep = true; } + + const parentSuite = suite.describe.suite; + const parentTestSuite = parentSuite && + TestSuiteInternal.suites.get(parentSuite.symbol); + if (parentTestSuite) { + TestSuiteInternal.addingOnlyStep(parentTestSuite); + } } /** This is used internally to add steps to a test suite. */ From a93cdf2af70cd33af6804a37cebac7588f3029e2 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Sun, 3 Apr 2022 16:11:16 -0500 Subject: [PATCH 12/16] Fix only on multiple tests in different suites --- testing/_test_suite.ts | 31 ++++++++++++++----------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/testing/_test_suite.ts b/testing/_test_suite.ts index 07f174be13a3..ae3c844e97f2 100644 --- a/testing/_test_suite.ts +++ b/testing/_test_suite.ts @@ -188,14 +188,8 @@ export class TestSuiteInternal implements TestSuite { if (!suite.hasOnlyStep) { for (let i = 0; i < suite.steps.length; i++) { const step = suite.steps[i]!; - if (step instanceof TestSuiteInternal) { - if (!(step.hasOnlyStep || step.describe.only)) { - suite.steps.splice(i--, 1); - } - } else { - if (!step.only) { - suite.steps.splice(i--, 1); - } + if (!(step instanceof TestSuiteInternal) && !step.only) { + suite.steps.splice(i--, 1); } } suite.hasOnlyStep = true; @@ -224,16 +218,11 @@ export class TestSuiteInternal implements TestSuite { } } - let omit = false; - if (suite.hasOnlyStep) { - if (step instanceof TestSuiteInternal) { - if (!(step.hasOnlyStep || step.describe.only)) omit = true; - } else { - if (!step.only) omit = true; - } + if ( + !(suite.hasOnlyStep && !(step instanceof TestSuiteInternal) && !step.only) + ) { + suite.steps.push(step); } - - if (!omit) suite.steps.push(step); } /** This is used internally to add hooks to a test suite. */ @@ -260,7 +249,15 @@ export class TestSuiteInternal implements TestSuite { context: T, t: Deno.TestContext, ): Promise { + const hasOnly = suite.hasOnlyStep || suite.describe.only || false; for (const step of suite.steps) { + if ( + hasOnly && step instanceof TestSuiteInternal && + !(step.hasOnlyStep || step.describe.only || false) + ) { + continue; + } + const { name, fn, From 27db3c3fbd1843bfb2e65e74322755032b185daf Mon Sep 17 00:00:00 2001 From: Kyle June Date: Wed, 6 Apr 2022 23:56:04 -0500 Subject: [PATCH 13/16] Make requested documentation changes and improve only option --- testing/README.md | 57 +++++++------- testing/_test_suite.ts | 5 +- testing/bdd_test.ts | 164 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 198 insertions(+), 28 deletions(-) diff --git a/testing/README.md b/testing/README.md index 706c40e3ccf7..6ce86f2b9c21 100644 --- a/testing/README.md +++ b/testing/README.md @@ -285,17 +285,15 @@ grouping tests and adding setup/teardown hooks used by other JavaScript testing frameworks like Jasmine, Jest, and Mocha. The `describe` function creates a block that groups together several related -tests. The `it` function registers an individual test case. The `describe` and -`it` functions have similar call signatures to `Deno.test`, making it easy to -migrate from using `Deno.test`. +tests. The `it` function registers an individual test case. ### Hooks There are 4 types of hooks available for test suites. A test suite can have multiples of each type of hook, they will be called in the order that they are registered. The `afterEach` and `afterAll` hooks will be called whether or not -the test case passes. The all hooks will be called once for the whole group -while the each hooks will be called for each individual test case. +the test case passes. The *All hooks will be called once for the whole group +while the *Each hooks will be called for each individual test case. - `beforeAll`: Runs before all of the tests in the test suite. - `afterAll`: Runs after all of the tests in the test suite finish. @@ -308,18 +306,21 @@ registered before any individual test cases or test suites. ### Focusing tests -If you would like to only run specific individual test cases, you can do so by -calling `it.only` instead of `it`. If you would like to only run specific test -suites, you can do so by calling `describe.only` instead of `describe`. +If you would like to run only specific test cases, you can do so by calling +`it.only` instead of `it`. If you would like to run only specific test suites, +you can do so by calling `describe.only` instead of `describe`. -There is one limitation to this when the individual test cases or test suites -belong to another test suite, they will be the only ones to run within the top -level test suite. +There is one limitation to this when using the flat test grouping style. When +`describe` is called without being nested, it registers the test with +`Deno.test`. If a child test case or suite is registered with `it.only` or +`describe.only`, it will be scoped to the top test suite instead of the file. To +make them the only tests that run in the file, you would need to register the +top test suite with `describe.only` too. ### Ignoring tests If you would like to not run specific individual test cases, you can do so by -calling `it.ignore` instead of `it`. If you would like to only run specific test +calling `it.ignore` instead of `it`. If you would like to not run specific test suites, you can do so by calling `describe.ignore` instead of `describe`. ### Sanitization options @@ -327,36 +328,40 @@ suites, you can do so by calling `describe.ignore` instead of `describe`. Like `Deno.TestDefinition`, the `DescribeDefinition` and `ItDefinition` have sanitization options. They work in the same way. -- sanitizeExit: Ensure the test case does not prematurely cause the process to +- `sanitizeExit`: Ensure the test case does not prematurely cause the process to exit, for example via a call to Deno.exit. Defaults to true. -- sanitizeOps: Check that the number of async completed ops after the test is +- `sanitizeOps`: Check that the number of async completed ops after the test is the same as number of dispatched ops. Defaults to true. -- sanitizeResources: Ensure the test case does not "leak" resources - ie. the +- `sanitizeResources`: Ensure the test case does not "leak" resources - ie. the resource table after the test has exactly the same contents as before the test. Defaults to true. ### Permissions option Like `Deno.TestDefinition`, the `DescribeDefintion` and `ItDefinition` have a -permissions option. They specify the permissions that should be used to run an -individual test case or test suite. Set this to "inherit" to keep the calling -thread's permissions. Set this to "none" to revoke all permissions. +`permissions` option. They specify the permissions that should be used to run an +individual test case or test suite. Set this to `"inherit"` to keep the calling +thread's permissions. Set this to `"none"` to revoke all permissions. -Defaults to "inherit". +This setting defaults to `"inherit"`. There is currently one limitation to this, you cannot use the permissions option on an individual test case or test suite that belongs to another test suite. +That's because internally those tests are registered with `t.step` which does +not support the permissions option. -### Migrating and usage +### Comparing to Deno\.test -To migrate from `Deno.test`, all you have to do is replace `Deno.test` with -`it`. If you are using the step API, you will need to replace `Deno.test` with -describe and steps with `describe` or `it`. The callback for individual test -cases can be syncronous or asyncronous. +The default way of writing tests is using `Deno.test` and `t.step`. The +`describe` and `it` functions have similar call signatures to `Deno.test`, +making it easy to switch between the default style and the behavior-driven +development style of writing tests. Internally, `describe` and `it` are +registering tests with `Deno.test` and `t.step`. Below is an example of a test file using `Deno.test` and `t.step`. In the -following sections there are examples of how it can be converted to using nested -test grouping, flat test grouping, and a mix of both. +following sections there are examples of how the same test could be written with +`describe` and `it` using nested test grouping, flat test grouping, or a mix of +both styles. ```ts // https://deno.land/std@$STD_VERSION/testing/bdd_examples/user_test.ts diff --git a/testing/_test_suite.ts b/testing/_test_suite.ts index ae3c844e97f2..29061bd0a677 100644 --- a/testing/_test_suite.ts +++ b/testing/_test_suite.ts @@ -105,12 +105,15 @@ export class TestSuiteInternal implements TestSuite { const { name, ignore, - only, permissions, sanitizeExit, sanitizeOps, sanitizeResources, } = describe; + let { only } = describe; + if (!ignore && this.hasOnlyStep) { + only = true; + } TestSuiteInternal.registerTest({ name, ignore, diff --git a/testing/bdd_test.ts b/testing/bdd_test.ts index 9c1a671a48e0..cbec633ac30b 100644 --- a/testing/bdd_test.ts +++ b/testing/bdd_test.ts @@ -155,6 +155,10 @@ Deno.test("global", async (t) => { }); await t.step("it", async (t) => { + /** + * Asserts that `Deno.test` is called with the correct options for the `it` call in the callback function. + * This is used to reduce code duplication when testing calling `it` with different call signatures. + */ async function assertOptions( expectedOptions: Omit, cb: (fn: Spy) => void, @@ -193,12 +197,20 @@ Deno.test("global", async (t) => { } } + /** + * Asserts that `Deno.test` is called with just the name and function for the `it` call in the callback function. + * This is used to reduce code duplication when testing calling `it` with different call signatures. + */ async function assertMinimumOptions( cb: (fn: Spy) => void, ): Promise { await assertOptions({}, cb); } + /** + * Asserts that `Deno.test` is called with all of the options for the `it` call in the callback function. + * This is used to reduce code duplication when testing calling `it` with different call signatures. + */ async function assertAllOptions( cb: (fn: Spy) => void, ): Promise { @@ -332,12 +344,20 @@ Deno.test("global", async (t) => { }); await t.step("only", async (t) => { + /** + * Asserts that `Deno.test` is called with just the name, only, and function for the `it.only` call in the callback function. + * This is used to reduce code duplication when testing calling `it.only` with different call signatures. + */ async function assertMinimumOptions( cb: (fn: Spy) => void, ): Promise { await assertOptions({ only: true }, cb); } + /** + * Asserts that `Deno.test` is called with all of the options for the `it.only` call in the callback function. + * This is used to reduce code duplication when testing calling `it.only` with different call signatures. + */ async function assertAllOptions( cb: (fn: Spy) => void, ): Promise { @@ -478,12 +498,20 @@ Deno.test("global", async (t) => { }); await t.step("ignore", async (t) => { + /** + * Asserts that `Deno.test` is called with just the name, ignore, and function for the `it.ignore` call in the callback function. + * This is used to reduce code duplication when testing calling `it.ignore` with different call signatures. + */ async function assertMinimumOptions( cb: (fn: Spy) => void, ): Promise { await assertOptions({ ignore: true }, cb); } + /** + * Asserts that `Deno.test` is called with all of the options for the `it.ignore` call in the callback function. + * This is used to reduce code duplication when testing calling `it.ignore` with different call signatures. + */ async function assertAllOptions( cb: (fn: Spy) => void, ): Promise { @@ -625,6 +653,11 @@ Deno.test("global", async (t) => { }); await t.step("describe", async (t) => { + /** + * Asserts that `Deno.test` is called with the correct options for the `describe` call in the callback function. + * In addition to that, it asserts that the individual test cases registered with `it` use the test step API correctly. + * This is used to reduce code duplication when testing calling `describe` with different call signatures. + */ async function assertOptions( expectedOptions: Omit, cb: (fns: Spy[]) => void, @@ -675,12 +708,22 @@ Deno.test("global", async (t) => { } } + /** + * Asserts that `Deno.test` is called with just the name and function for the `describe` call in the callback function. + * In addition to that, it asserts that the individual test cases registered with `it` use the test step API correctly. + * This is used to reduce code duplication when testing calling `describe` with different call signatures. + */ async function assertMinimumOptions( cb: (fns: Spy[]) => void, ): Promise { await assertOptions({}, cb); } + /** + * Asserts that `Deno.test` is called with all of the options for the `describe` call in the callback function. + * In addition to that, it asserts that the individual test cases registered with `it` use the test step API correctly. + * This is used to reduce code duplication when testing calling `describe` with different call signatures. + */ async function assertAllOptions( cb: (fns: Spy[]) => void, ): Promise { @@ -850,12 +893,22 @@ Deno.test("global", async (t) => { }); await t.step("only", async (t) => { + /** + * Asserts that `Deno.test` is called with just the name, only, and function for the `describe.only` call in the callback function. + * In addition to that, it asserts that the individual test cases registered with `it` use the test step API correctly. + * This is used to reduce code duplication when testing calling `describe.only` with different call signatures. + */ async function assertMinimumOptions( cb: (fns: Spy[]) => void, ): Promise { await assertOptions({ only: true }, cb); } + /** + * Asserts that `Deno.test` is called with all of the options for the `describe.only` call in the callback function. + * In addition to that, it asserts that the individual test cases registered with `it` use the test step API correctly. + * This is used to reduce code duplication when testing calling `describe.only` with different call signatures. + */ async function assertAllOptions( cb: (fns: Spy[]) => void, ): Promise { @@ -1041,12 +1094,22 @@ Deno.test("global", async (t) => { }); await t.step("ignore", async (t) => { + /** + * Asserts that `Deno.test` is called with just the name, ignore, and function for the `describe.ignore` call in the callback function. + * In addition to that, it asserts that the individual test cases registered with `it` use the test step API correctly. + * This is used to reduce code duplication when testing calling `describe.ignore` with different call signatures. + */ async function assertMinimumOptions( cb: (fns: Spy[]) => void, ): Promise { await assertOptions({ ignore: true }, cb); } + /** + * Asserts that `Deno.test` is called with all of the options for the `describe.ignore` call in the callback function. + * In addition to that, it asserts that the individual test cases registered with `it` use the test step API correctly. + * This is used to reduce code duplication when testing calling `describe.ignore` with different call signatures. + */ async function assertAllOptions( cb: (fns: Spy[]) => void, ): Promise { @@ -1232,6 +1295,10 @@ Deno.test("global", async (t) => { }); await t.step("nested only", async (t) => { + /** + * Asserts that when only is used on a nested `describe` or `it` call, it will be the only test case or suite that runs in the file. + * This is used to reduce code duplication when testing calling `describe.ignore` with different call signatures. + */ async function assertOnly( cb: (fns: Spy[]) => void, ): Promise { @@ -1245,10 +1312,11 @@ Deno.test("global", async (t) => { const options = call.args[0] as Deno.TestDefinition; assertEquals( Object.keys(options).sort(), - ["name", "fn"].sort(), + ["name", "only", "fn"].sort(), ); assertObjectMatch(options, { name: "example", + only: true, }); assertSpyCalls(fns[0], 0); @@ -1324,7 +1392,101 @@ Deno.test("global", async (t) => { })); }); + await t.step("flat child only", async (t) => { + /** + * Asserts that when only is used on a child `describe` or `it` call, it will be the only test case or suite that runs within the top test suite. + * This demonstrates the issue where `Deno.test` is called without `only` even though one of it's child steps are focused. + * This is used to reduce code duplication when testing calling `describe.ignore` with different call signatures. + */ + async function assertOnly( + cb: (fns: Spy[]) => void, + ): Promise { + const test = stub(Deno, "test"); + const fns = [spy(), spy(), spy()]; + try { + cb(fns); + + assertSpyCall(test, 0); + const call = test.calls[0]; + const options = call.args[0] as Deno.TestDefinition; + assertEquals( + Object.keys(options).sort(), + ["name", "fn"].sort(), + ); + assertObjectMatch(options, { + name: "example", + }); + + assertSpyCalls(fns[0], 0); + assertSpyCalls(fns[1], 0); + + const context = new TestContext(); + const result = options.fn(context); + assertStrictEquals(Promise.resolve(result), result); + assertEquals(await result, undefined); + assertSpyCalls(context.spies.step, 1); + + let fn = fns[0]; + assertSpyCalls(fn, 0); + + fn = fns[1]; + assertSpyCall(fn, 0, { + self: {}, + args: [], + returned: undefined, + }); + assertSpyCalls(fn, 1); + + fn = fns[2]; + assertSpyCalls(fn, 0); + } finally { + TestSuiteInternal.reset(); + test.restore(); + } + } + + await t.step("it", async () => + await assertOnly((fns) => { + const suite = describe("example"); + assertEquals(it({ name: "a", suite, fn: fns[0] }), undefined); + assertEquals(it.only({ name: "b", suite, fn: fns[1] }), undefined); + assertEquals(it({ name: "c", suite, fn: fns[2] }), undefined); + })); + + await t.step("deep child it", async () => + await assertOnly((fns) => { + const suite = describe("example") + assertEquals(it({ name: "a", suite, fn: fns[0] }), undefined); + const childSuite = describe(suite, "child"); + assertEquals(it.only({ name: "b", suite: childSuite, fn: fns[1] }), undefined); + assertEquals(it({ name: "c", suite, fn: fns[2] }), undefined); + })); + + await t.step("describe", async () => + await assertOnly((fns) => { + const suite = describe("example"); + assertEquals(it({ name: "a", suite, fn: fns[0] }), undefined); + const childSuite = describe.only(suite, "child") + assertEquals(it({ name: "b", suite: childSuite, fn: fns[1] }), undefined); + assertEquals(it({ name: "c", suite, fn: fns[2] }), undefined); + })); + + await t.step("deep child describe", async () => + await assertOnly((fns) => { + const suite = describe("example"); + assertEquals(it({ name: "a", suite, fn: fns[0] }), undefined); + const childSuite = describe(suite, "child"); + const child2Suite = describe.only(childSuite, "child 2"); + assertEquals(it({ name: "b", suite: child2Suite, fn: fns[1] }), undefined); + assertEquals(it({ name: "c", suite, fn: fns[2] }), undefined); + })); + }); + await t.step("with hooks", async (t) => { + /** + * Asserts that all the different hook types are called in the correct order when the tests run. + * This is used to reduce code duplication when testing calling `describe` with different call signatures. + */ async function assertHooks( cb: ( options: { From 84fd424925a3c616f94faa5f904fcfa81ad1acc1 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Wed, 6 Apr 2022 23:59:02 -0500 Subject: [PATCH 14/16] Fix formatting --- testing/bdd_test.ts | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/testing/bdd_test.ts b/testing/bdd_test.ts index cbec633ac30b..ef97f02f68a9 100644 --- a/testing/bdd_test.ts +++ b/testing/bdd_test.ts @@ -1455,10 +1455,13 @@ Deno.test("global", async (t) => { await t.step("deep child it", async () => await assertOnly((fns) => { - const suite = describe("example") + const suite = describe("example"); assertEquals(it({ name: "a", suite, fn: fns[0] }), undefined); const childSuite = describe(suite, "child"); - assertEquals(it.only({ name: "b", suite: childSuite, fn: fns[1] }), undefined); + assertEquals( + it.only({ name: "b", suite: childSuite, fn: fns[1] }), + undefined, + ); assertEquals(it({ name: "c", suite, fn: fns[2] }), undefined); })); @@ -1466,20 +1469,29 @@ Deno.test("global", async (t) => { await assertOnly((fns) => { const suite = describe("example"); assertEquals(it({ name: "a", suite, fn: fns[0] }), undefined); - const childSuite = describe.only(suite, "child") - assertEquals(it({ name: "b", suite: childSuite, fn: fns[1] }), undefined); + const childSuite = describe.only(suite, "child"); + assertEquals( + it({ name: "b", suite: childSuite, fn: fns[1] }), + undefined, + ); assertEquals(it({ name: "c", suite, fn: fns[2] }), undefined); })); - await t.step("deep child describe", async () => - await assertOnly((fns) => { - const suite = describe("example"); - assertEquals(it({ name: "a", suite, fn: fns[0] }), undefined); - const childSuite = describe(suite, "child"); - const child2Suite = describe.only(childSuite, "child 2"); - assertEquals(it({ name: "b", suite: child2Suite, fn: fns[1] }), undefined); - assertEquals(it({ name: "c", suite, fn: fns[2] }), undefined); - })); + await t.step( + "deep child describe", + async () => + await assertOnly((fns) => { + const suite = describe("example"); + assertEquals(it({ name: "a", suite, fn: fns[0] }), undefined); + const childSuite = describe(suite, "child"); + const child2Suite = describe.only(childSuite, "child 2"); + assertEquals( + it({ name: "b", suite: child2Suite, fn: fns[1] }), + undefined, + ); + assertEquals(it({ name: "c", suite, fn: fns[2] }), undefined); + }), + ); }); await t.step("with hooks", async (t) => { From 6583cea3692fcbeb5b88492cde948adbba3c5b1e Mon Sep 17 00:00:00 2001 From: Kyle June Date: Thu, 7 Apr 2022 00:09:48 -0500 Subject: [PATCH 15/16] Add Deno copyright --- testing/_test_suite.ts | 1 + testing/_test_utils.ts | 1 + testing/bdd.ts | 1 + testing/bdd_examples/user.ts | 1 + testing/bdd_examples/user_flat_test.ts | 1 + testing/bdd_examples/user_mixed_test.ts | 1 + testing/bdd_examples/user_nested_test.ts | 1 + testing/bdd_examples/user_test.ts | 1 + testing/bdd_test.ts | 1 + testing/mock.ts | 1 + testing/mock_examples/internals_injection.ts | 1 + testing/mock_examples/internals_injection_test.ts | 1 + testing/mock_examples/parameter_injection.ts | 1 + testing/mock_examples/parameter_injection_test.ts | 1 + testing/mock_examples/random.ts | 1 + testing/mock_examples/random_test.ts | 1 + testing/mock_test.ts | 1 + 17 files changed, 17 insertions(+) diff --git a/testing/_test_suite.ts b/testing/_test_suite.ts index 29061bd0a677..a26129aa7031 100644 --- a/testing/_test_suite.ts +++ b/testing/_test_suite.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. /** The options for creating a test suite with the describe function. */ export interface DescribeDefinition extends Omit { fn?: () => void; diff --git a/testing/_test_utils.ts b/testing/_test_utils.ts index 9f8b2e7d39a5..b644b2b3f3f1 100755 --- a/testing/_test_utils.ts +++ b/testing/_test_utils.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. export class Point { constructor(public x: number, public y: number) {} // deno-lint-ignore no-explicit-any diff --git a/testing/bdd.ts b/testing/bdd.ts index 142a02fd341c..1d9d6ad75666 100644 --- a/testing/bdd.ts +++ b/testing/bdd.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { DescribeDefinition, HookNames, diff --git a/testing/bdd_examples/user.ts b/testing/bdd_examples/user.ts index ae1ce2091a21..0c33e14e7256 100644 --- a/testing/bdd_examples/user.ts +++ b/testing/bdd_examples/user.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. export class User { static users: Map = new Map(); age?: number; diff --git a/testing/bdd_examples/user_flat_test.ts b/testing/bdd_examples/user_flat_test.ts index 61bd7faf1770..c12728c55f37 100644 --- a/testing/bdd_examples/user_flat_test.ts +++ b/testing/bdd_examples/user_flat_test.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; import { describe, it } from "../bdd.ts"; import { User } from "./user.ts"; diff --git a/testing/bdd_examples/user_mixed_test.ts b/testing/bdd_examples/user_mixed_test.ts index ea2a21df8ac8..09023c183a04 100644 --- a/testing/bdd_examples/user_mixed_test.ts +++ b/testing/bdd_examples/user_mixed_test.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; import { describe, it } from "../bdd.ts"; import { User } from "./user.ts"; diff --git a/testing/bdd_examples/user_nested_test.ts b/testing/bdd_examples/user_nested_test.ts index 4463fe02a5c4..9d07a7eed2d3 100644 --- a/testing/bdd_examples/user_nested_test.ts +++ b/testing/bdd_examples/user_nested_test.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; import { afterEach, beforeEach, describe, it } from "../bdd.ts"; import { User } from "./user.ts"; diff --git a/testing/bdd_examples/user_test.ts b/testing/bdd_examples/user_test.ts index dffd31727344..26154d95cd8b 100644 --- a/testing/bdd_examples/user_test.ts +++ b/testing/bdd_examples/user_test.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { assertEquals, assertStrictEquals, assertThrows } from "../asserts.ts"; import { User } from "./user.ts"; diff --git a/testing/bdd_test.ts b/testing/bdd_test.ts index ef97f02f68a9..df329f17e2f9 100644 --- a/testing/bdd_test.ts +++ b/testing/bdd_test.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { assert, assertEquals, diff --git a/testing/mock.ts b/testing/mock.ts index 75e8f0656750..64e27a5a2eb6 100644 --- a/testing/mock.ts +++ b/testing/mock.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. /** This module is browser compatible. */ import { diff --git a/testing/mock_examples/internals_injection.ts b/testing/mock_examples/internals_injection.ts index 43844129dd3e..802fc97aded8 100644 --- a/testing/mock_examples/internals_injection.ts +++ b/testing/mock_examples/internals_injection.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. export function multiply(a: number, b: number): number { return a * b; } diff --git a/testing/mock_examples/internals_injection_test.ts b/testing/mock_examples/internals_injection_test.ts index 14b939b71bba..0b7df51e7173 100644 --- a/testing/mock_examples/internals_injection_test.ts +++ b/testing/mock_examples/internals_injection_test.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { assertSpyCall, assertSpyCalls, spy } from "../mock.ts"; import { assertEquals } from "../asserts.ts"; import { _internals, square } from "./internals_injection.ts"; diff --git a/testing/mock_examples/parameter_injection.ts b/testing/mock_examples/parameter_injection.ts index 64aee2e173f2..47604fbba4e3 100644 --- a/testing/mock_examples/parameter_injection.ts +++ b/testing/mock_examples/parameter_injection.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. export function multiply(a: number, b: number): number { return a * b; } diff --git a/testing/mock_examples/parameter_injection_test.ts b/testing/mock_examples/parameter_injection_test.ts index 1ec580ea27af..b6b45d2a620d 100644 --- a/testing/mock_examples/parameter_injection_test.ts +++ b/testing/mock_examples/parameter_injection_test.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { assertSpyCall, assertSpyCalls, spy } from "../mock.ts"; import { assertEquals } from "../asserts.ts"; import { multiply, square } from "./parameter_injection.ts"; diff --git a/testing/mock_examples/random.ts b/testing/mock_examples/random.ts index 06e98a345de1..655bd98b7c3e 100644 --- a/testing/mock_examples/random.ts +++ b/testing/mock_examples/random.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. export function randomInt(lowerBound: number, upperBound: number): number { return lowerBound + Math.floor(Math.random() * (upperBound - lowerBound)); } diff --git a/testing/mock_examples/random_test.ts b/testing/mock_examples/random_test.ts index 8558629072e6..dec161ea1dc9 100644 --- a/testing/mock_examples/random_test.ts +++ b/testing/mock_examples/random_test.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { assertSpyCall, assertSpyCalls, returnsNext, stub } from "../mock.ts"; import { assertEquals } from "../asserts.ts"; import { _internals, randomMultiple } from "./random.ts"; diff --git a/testing/mock_test.ts b/testing/mock_test.ts index 7cf1f6ef0131..b7beaffc98be 100644 --- a/testing/mock_test.ts +++ b/testing/mock_test.ts @@ -1,3 +1,4 @@ +// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. import { delay } from "../async/delay.ts"; import { assertEquals, From 1079bb37ef4d0cc1f2e4c3605e912dc4c43cf0a4 Mon Sep 17 00:00:00 2001 From: Kyle June Date: Thu, 7 Apr 2022 00:31:16 -0500 Subject: [PATCH 16/16] Fix fake TestContext for canary --- testing/bdd_test.ts | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/testing/bdd_test.ts b/testing/bdd_test.ts index df329f17e2f9..89620893b294 100644 --- a/testing/bdd_test.ts +++ b/testing/bdd_test.ts @@ -18,12 +18,16 @@ import { assertSpyCall, assertSpyCalls, Spy, spy, stub } from "./mock.ts"; Deno.test("global", async (t) => { class TestContext implements Deno.TestContext { + name: string; + origin: string; steps: TestContext[]; spies: { step: Spy; }; - constructor() { + constructor(name: string) { + this.name = name; + this.origin = "origin"; this.spies = { step: spy(this, "step"), }; @@ -45,7 +49,8 @@ Deno.test("global", async (t) => { fn = tOrName.fn; } - const context = new TestContext(); + const name = typeof tOrName === "string" ? tOrName : tOrName.name; + const context = new TestContext(name); this.steps.push(context); if (!ignore) { await fn!(context); @@ -123,7 +128,7 @@ Deno.test("global", async (t) => { assertEquals(Object.keys(options).sort(), ["fn", "name"]); assertEquals(options.name, "global"); - const context = new TestContext(); + const context = new TestContext("global"); const result = options.fn(context); assertStrictEquals(Promise.resolve(result), result); assertEquals(await result, undefined); @@ -182,7 +187,7 @@ Deno.test("global", async (t) => { ...expectedOptions, }); - const context = new TestContext(); + const context = new TestContext("example"); const result = options.fn(context); assertStrictEquals(Promise.resolve(result), result); assertEquals(await result, undefined); @@ -683,7 +688,7 @@ Deno.test("global", async (t) => { assertSpyCalls(fns[0], 0); assertSpyCalls(fns[1], 0); - const context = new TestContext(); + const context = new TestContext("example"); const result = options.fn(context); assertStrictEquals(Promise.resolve(result), result); assertEquals(await result, undefined); @@ -1323,7 +1328,7 @@ Deno.test("global", async (t) => { assertSpyCalls(fns[0], 0); assertSpyCalls(fns[1], 0); - const context = new TestContext(); + const context = new TestContext("example"); const result = options.fn(context); assertStrictEquals(Promise.resolve(result), result); assertEquals(await result, undefined); @@ -1421,7 +1426,7 @@ Deno.test("global", async (t) => { assertSpyCalls(fns[0], 0); assertSpyCalls(fns[1], 0); - const context = new TestContext(); + const context = new TestContext("example"); const result = options.fn(context); assertStrictEquals(Promise.resolve(result), result); assertEquals(await result, undefined); @@ -1527,7 +1532,7 @@ Deno.test("global", async (t) => { assertEquals(Object.keys(options).sort(), ["fn", "name"]); assertEquals(options.name, "example"); - const context = new TestContext(); + const context = new TestContext("example"); const result = options.fn(context); assertStrictEquals(Promise.resolve(result), result); assertEquals(await result, undefined); @@ -1634,7 +1639,7 @@ Deno.test("global", async (t) => { assertEquals(Object.keys(options).sort(), ["fn", "name"]); assertEquals(options.name, "example"); - let context = new TestContext(); + let context = new TestContext("example"); const result = options.fn(context); assertStrictEquals(Promise.resolve(result), result); assertEquals(await result, undefined); @@ -1741,7 +1746,7 @@ Deno.test("global", async (t) => { assertEquals(Object.keys(options).sort(), ["fn", "name"]); assertEquals(options.name, "example"); - let context = new TestContext(); + let context = new TestContext("example"); const result = options.fn(context); assertStrictEquals(Promise.resolve(result), result); assertEquals(await result, undefined);