From 3ab866bd72fd3eecaf6c7f734fb66819ccaf192e Mon Sep 17 00:00:00 2001 From: hasundue Date: Fri, 27 Oct 2023 12:00:53 +0900 Subject: [PATCH] feat(testing/mock): spy constructor --- testing/mock.ts | 108 ++++++++++++++++++++++++++++++++++++------- testing/mock_test.ts | 60 ++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 17 deletions(-) diff --git a/testing/mock.ts b/testing/mock.ts index ee2bf5cbefdb..3b1a35900186 100644 --- a/testing/mock.ts +++ b/testing/mock.ts @@ -555,6 +555,58 @@ function methodSpy< return spy; } +/** A constructor wrapper that records all calls made to it. */ +export interface ConstructorSpy< + // deno-lint-ignore no-explicit-any + Self = any, + // deno-lint-ignore no-explicit-any + Args extends unknown[] = any[], +> { + new (...args: Args): Self; + /** The function that is being spied on. */ + original: new (...args: Args) => Self; + /** 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 constructor with a Spy. */ +function constructorSpy< + Self, + Args extends unknown[], +>( + constructor: new (...args: Args) => Self, +): ConstructorSpy { + const original = constructor, + calls: SpyCall[] = []; + // @ts-ignore TS2509: Can't know the type of `original` statically. + const spy = class extends original { + constructor(...args: Args) { + super(...args); + const call: SpyCall = { args }; + try { + call.returned = this as unknown as Self; + } catch (error) { + call.error = error as Error; + calls.push(call); + throw error; + } + calls.push(call); + } + static readonly name = original.name; + static readonly original = original; + static readonly calls = calls; + static readonly restored = false; + static restore() { + throw new MockError("constructor cannot be restored"); + } + } as ConstructorSpy; + return spy; +} + /** Utility for extracting the arguments type from a property */ type GetParametersFromProp< Self, @@ -570,6 +622,15 @@ type GetReturnFromProp< = Self[Prop] extends (...args: any[]) => infer Return ? Return : unknown; +type SpyLike< + // 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, +> = Spy | ConstructorSpy; + /** Wraps a function or instance method with a Spy. */ export function spy< // deno-lint-ignore no-explicit-any @@ -583,6 +644,13 @@ export function spy< Args extends unknown[], Return, >(func: (this: Self, ...args: Args) => Return): Spy; +export function spy< + Self, + Args extends unknown[], + Return = undefined, +>( + constructor: new (...args: Args) => Self, +): ConstructorSpy; export function spy< Self, Prop extends keyof Self, @@ -595,17 +663,23 @@ export function spy< Args extends unknown[], Return, >( - funcOrSelf?: ((this: Self, ...args: Args) => Return) | Self, + funcOrConstOrSelf?: + | ((this: Self, ...args: Args) => Return) + | (new (...args: Args) => Self) + | 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, +): SpyLike { + return !funcOrConstOrSelf + ? functionSpy() + : property !== undefined + ? methodSpy(funcOrConstOrSelf as Self, property) + : funcOrConstOrSelf.toString().startsWith("class") + ? constructorSpy( + funcOrConstOrSelf as new (...args: Args) => Self, ) - : functionSpy(); - return spy; + : functionSpy( + funcOrConstOrSelf as (this: Self, ...args: Args) => Return, + ); } /** An instance method replacement that records all calls made to it. */ @@ -735,7 +809,7 @@ export function assertSpyCalls< Args extends unknown[], Return, >( - spy: Spy, + spy: SpyLike, expectedCalls: number, ) { try { @@ -785,7 +859,7 @@ export function assertSpyCall< Args extends unknown[], Return, >( - spy: Spy, + spy: SpyLike, callIndex: number, expected?: ExpectedSpyCall, ) { @@ -864,7 +938,7 @@ export async function assertSpyCallAsync< Args extends unknown[], Return, >( - spy: Spy>, + spy: SpyLike>, callIndex: number, expected?: ExpectedSpyCall | Return>, ) { @@ -945,7 +1019,7 @@ export function assertSpyCallArg< Return, ExpectedArg, >( - spy: Spy, + spy: SpyLike, callIndex: number, argIndex: number, expected: ExpectedArg, @@ -969,7 +1043,7 @@ export function assertSpyCallArgs< Return, ExpectedArgs extends unknown[], >( - spy: Spy, + spy: SpyLike, callIndex: number, expected: ExpectedArgs, ): ExpectedArgs; @@ -979,7 +1053,7 @@ export function assertSpyCallArgs< Return, ExpectedArgs extends unknown[], >( - spy: Spy, + spy: SpyLike, callIndex: number, argsStart: number, expected: ExpectedArgs, @@ -990,7 +1064,7 @@ export function assertSpyCallArgs< Return, ExpectedArgs extends unknown[], >( - spy: Spy, + spy: SpyLike, callIndex: number, argStart: number, argEnd: number, @@ -1002,7 +1076,7 @@ export function assertSpyCallArgs< Return, Self, >( - spy: Spy, + spy: SpyLike, callIndex: number, argsStart?: number | ExpectedArgs, argsEnd?: number | ExpectedArgs, diff --git a/testing/mock_test.ts b/testing/mock_test.ts index 00da45e30dc7..003569357584 100644 --- a/testing/mock_test.ts +++ b/testing/mock_test.ts @@ -345,6 +345,66 @@ Deno.test("spy instance method property descriptor", () => { assertEquals(action.restored, true); }); +Deno.test("spy constructor", () => { + const PointSpy = spy(Point); + assertSpyCalls(PointSpy, 0); + + const point = new PointSpy(2, 3); + assertEquals(point.x, 2); + assertEquals(point.y, 3); + assertEquals(point.action(), undefined); + + assertSpyCall(PointSpy, 0, { + self: undefined, + args: [2, 3], + returned: point, + }); + assertSpyCallArg(PointSpy, 0, 0, 2); + assertSpyCallArgs(PointSpy, 0, 0, 1, [2]); + assertSpyCalls(PointSpy, 1); + + new PointSpy(3, 5); + assertSpyCall(PointSpy, 1, { + self: undefined, + args: [3, 5], + }); + assertSpyCalls(PointSpy, 2); + + assertThrows( + () => PointSpy.restore(), + MockError, + "constructor cannot be restored", + ); +}); + +Deno.test("spy constructor of child class", () => { + const PointSpy = spy(Point); + const PointSpyChild = class extends PointSpy { + override action() { + return 1; + } + }; + const point = new PointSpyChild(2, 3); + + assertEquals(point.x, 2); + assertEquals(point.y, 3); + assertEquals(point.action(), 1); + + assertSpyCall(PointSpyChild, 0, { + self: undefined, + args: [2, 3], + returned: point, + }); + assertSpyCalls(PointSpyChild, 1); + + assertSpyCall(PointSpy, 0, { + self: undefined, + args: [2, 3], + returned: point, + }); + assertSpyCalls(PointSpy, 1); +}); + Deno.test("stub default", () => { const point = new Point(2, 3); const func = stub(point, "action");