Skip to content

Commit

Permalink
feat(testing/mock): spy constructor
Browse files Browse the repository at this point in the history
  • Loading branch information
hasundue committed Nov 2, 2023
1 parent 0a0cfae commit 5a8f3f6
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 18 deletions.
114 changes: 96 additions & 18 deletions testing/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,67 @@ 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<Self, Args, Self>[];
/** 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;
}

export 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<Self, Args, Return> | ConstructorSpy<Self, Args>;

/** Wraps a constructor with a Spy. */
function constructorSpy<
Self,
Args extends unknown[],
>(
constructor: new (...args: Args) => Self,
): ConstructorSpy<Self, Args> {
const original = constructor,
calls: SpyCall<Self, Args, Self>[] = [];
// @ts-ignore TS2509: Can't know the type of `original` statically.
const spy = class extends original {
constructor(...args: Args) {
super(...args);
const call: SpyCall<Self, Args, Self> = { args };
try {
call.returned = this as unknown as Self;
} catch (error) {
call.error = error as Error;
calls.push(call);
throw error;
}

Check warning on line 605 in testing/mock.ts

View check run for this annotation

Codecov / codecov/patch

testing/mock.ts#L602-L605

Added lines #L602 - L605 were not covered by tests
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<Self, Args>;
return spy;
}

/** Utility for extracting the arguments type from a property */
type GetParametersFromProp<
Self,
Expand Down Expand Up @@ -583,29 +644,46 @@ export function spy<
Args extends unknown[],
Return,
>(func: (this: Self, ...args: Args) => Return): Spy<Self, Args, Return>;
export function spy<
Self,
Args extends unknown[],
Return = undefined,
>(
constructor: new (...args: Args) => Self,
): ConstructorSpy<Self, Args>;
export function spy<
Self,
Prop extends keyof Self,
>(
self: Self,
property: Prop,
): Spy<Self, GetParametersFromProp<Self, Prop>, GetReturnFromProp<Self, Prop>>;
): Spy<
Self,
GetParametersFromProp<Self, Prop>,
GetReturnFromProp<Self, Prop>
>;
export function spy<
Self,
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<Self, Args, Return> {
const spy = typeof property !== "undefined"
? methodSpy<Self, Args, Return>(funcOrSelf as Self, property)
: typeof funcOrSelf === "function"
? functionSpy<Self, Args, Return>(
funcOrSelf as (this: Self, ...args: Args) => Return,
): SpyLike<Self, Args, Return> {
return !funcOrConstOrSelf
? functionSpy<Self, Args, Return>()
: property !== undefined
? methodSpy<Self, Args, Return>(funcOrConstOrSelf as Self, property)
: funcOrConstOrSelf.toString().startsWith("class")
? constructorSpy<Self, Args>(
funcOrConstOrSelf as new (...args: Args) => Self,
)
: functionSpy<Self, Args, Return>();
return spy;
: functionSpy<Self, Args, Return>(
funcOrConstOrSelf as (this: Self, ...args: Args) => Return,
);
}

/** An instance method replacement that records all calls made to it. */
Expand Down Expand Up @@ -735,7 +813,7 @@ export function assertSpyCalls<
Args extends unknown[],
Return,
>(
spy: Spy<Self, Args, Return>,
spy: SpyLike<Self, Args, Return>,
expectedCalls: number,
) {
try {
Expand Down Expand Up @@ -785,7 +863,7 @@ export function assertSpyCall<
Args extends unknown[],
Return,
>(
spy: Spy<Self, Args, Return>,
spy: SpyLike<Self, Args, Return>,
callIndex: number,
expected?: ExpectedSpyCall<Self, Args, Return>,
) {
Expand Down Expand Up @@ -864,7 +942,7 @@ export async function assertSpyCallAsync<
Args extends unknown[],
Return,
>(
spy: Spy<Self, Args, Promise<Return>>,
spy: SpyLike<Self, Args, Promise<Return>>,
callIndex: number,
expected?: ExpectedSpyCall<Self, Args, Promise<Return> | Return>,
) {
Expand Down Expand Up @@ -945,7 +1023,7 @@ export function assertSpyCallArg<
Return,
ExpectedArg,
>(
spy: Spy<Self, Args, Return>,
spy: SpyLike<Self, Args, Return>,
callIndex: number,
argIndex: number,
expected: ExpectedArg,
Expand All @@ -969,7 +1047,7 @@ export function assertSpyCallArgs<
Return,
ExpectedArgs extends unknown[],
>(
spy: Spy<Self, Args, Return>,
spy: SpyLike<Self, Args, Return>,
callIndex: number,
expected: ExpectedArgs,
): ExpectedArgs;
Expand All @@ -979,7 +1057,7 @@ export function assertSpyCallArgs<
Return,
ExpectedArgs extends unknown[],
>(
spy: Spy<Self, Args, Return>,
spy: SpyLike<Self, Args, Return>,
callIndex: number,
argsStart: number,
expected: ExpectedArgs,
Expand All @@ -990,7 +1068,7 @@ export function assertSpyCallArgs<
Return,
ExpectedArgs extends unknown[],
>(
spy: Spy<Self, Args, Return>,
spy: SpyLike<Self, Args, Return>,
callIndex: number,
argStart: number,
argEnd: number,
Expand All @@ -1002,7 +1080,7 @@ export function assertSpyCallArgs<
Return,
Self,
>(
spy: Spy<Self, Args, Return>,
spy: SpyLike<Self, Args, Return>,
callIndex: number,
argsStart?: number | ExpectedArgs,
argsEnd?: number | ExpectedArgs,
Expand Down
60 changes: 60 additions & 0 deletions testing/mock_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit 5a8f3f6

Please sign in to comment.