mirror of
https://github.com/denoland/std.git
synced 2024-11-21 12:40:03 +00:00
feat(testing/unstable): support for stubbing properties (#6128)
Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
This commit is contained in:
parent
2d9e21267d
commit
6cc4f9fc44
43
testing/_mock_utils.ts
Normal file
43
testing/_mock_utils.ts
Normal file
@ -0,0 +1,43 @@
|
||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
import type { Spy } from "./mock.ts";
|
||||
|
||||
/**
|
||||
* Checks if a function is a spy.
|
||||
*
|
||||
* @typeParam Self The self type of the function.
|
||||
* @typeParam Args The arguments type of the function.
|
||||
* @typeParam Return The return type of the function.
|
||||
* @param func The function to check
|
||||
* @return `true` if the function is a spy, `false` otherwise.
|
||||
*/
|
||||
export function isSpy<Self, Args extends unknown[], Return>(
|
||||
func: ((this: Self, ...args: Args) => Return) | unknown,
|
||||
): func is Spy<Self, Args, Return> {
|
||||
const spy = func as Spy<Self, Args, Return>;
|
||||
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
|
||||
export const sessions: Set<Spy<any, any[], any>>[] = [];
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
function getSession(): Set<Spy<any, any[], any>> {
|
||||
if (sessions.length === 0) sessions.push(new Set());
|
||||
return sessions.at(-1)!;
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
export function registerMock(spy: Spy<any, any[], any>) {
|
||||
const session = getSession();
|
||||
session.add(spy);
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
export function unregisterMock(spy: Spy<any, any[], any>) {
|
||||
const session = getSession();
|
||||
session.delete(spy);
|
||||
}
|
@ -7,6 +7,7 @@
|
||||
"./snapshot": "./snapshot.ts",
|
||||
"./time": "./time.ts",
|
||||
"./types": "./types.ts",
|
||||
"./unstable-types": "./unstable_types.ts"
|
||||
"./unstable-types": "./unstable_types.ts",
|
||||
"./unstable-stub": "./unstable_stub.ts"
|
||||
}
|
||||
}
|
||||
|
@ -323,6 +323,12 @@ import { assertEquals } from "@std/assert/equals";
|
||||
import { assertIsError } from "@std/assert/is-error";
|
||||
import { assertRejects } from "@std/assert/rejects";
|
||||
import { AssertionError } from "@std/assert/assertion-error";
|
||||
import {
|
||||
isSpy,
|
||||
registerMock,
|
||||
sessions,
|
||||
unregisterMock,
|
||||
} from "./_mock_utils.ts";
|
||||
|
||||
/**
|
||||
* An error related to spying on a function or instance method.
|
||||
@ -444,44 +450,6 @@ function functionSpy<
|
||||
return spy;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a function is a spy.
|
||||
*
|
||||
* @typeParam Self The self type of the function.
|
||||
* @typeParam Args The arguments type of the function.
|
||||
* @typeParam Return The return type of the function.
|
||||
* @param func The function to check
|
||||
* @return `true` if the function is a spy, `false` otherwise.
|
||||
*/
|
||||
function isSpy<Self, Args extends unknown[], Return>(
|
||||
func: ((this: Self, ...args: Args) => Return) | unknown,
|
||||
): func is Spy<Self, Args, Return> {
|
||||
const spy = func as Spy<Self, Args, Return>;
|
||||
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<Spy<any, any[], any>>[] = [];
|
||||
// deno-lint-ignore no-explicit-any
|
||||
function getSession(): Set<Spy<any, any[], any>> {
|
||||
if (sessions.length === 0) sessions.push(new Set());
|
||||
return sessions.at(-1)!;
|
||||
}
|
||||
// deno-lint-ignore no-explicit-any
|
||||
function registerMock(spy: Spy<any, any[], any>) {
|
||||
const session = getSession();
|
||||
session.add(spy);
|
||||
}
|
||||
// deno-lint-ignore no-explicit-any
|
||||
function unregisterMock(spy: Spy<any, any[], any>) {
|
||||
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.
|
||||
|
283
testing/unstable_stub.ts
Normal file
283
testing/unstable_stub.ts
Normal file
@ -0,0 +1,283 @@
|
||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
import { isSpy, registerMock, unregisterMock } from "./_mock_utils.ts";
|
||||
import {
|
||||
type GetParametersFromProp,
|
||||
type GetReturnFromProp,
|
||||
type MethodSpy,
|
||||
MockError,
|
||||
type Spy,
|
||||
spy,
|
||||
type SpyCall,
|
||||
} from "./mock.ts";
|
||||
|
||||
/** 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 MethodSpy<Self, Args, Return> {
|
||||
/** The function that is used instead of the original. */
|
||||
fake: (this: Self, ...args: Args) => Return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces an instance method with a Stub with empty implementation.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* import { assertSpyCalls } from "@std/testing/mock";
|
||||
* import { stub } from "@std/testing/unstable-stub";
|
||||
*
|
||||
* const obj = {
|
||||
* method() {
|
||||
* // some inconventient feature for testing
|
||||
* },
|
||||
* };
|
||||
*
|
||||
* const methodStub = stub(obj, "method");
|
||||
*
|
||||
* for (const _ of Array(5)) {
|
||||
* obj.method();
|
||||
* }
|
||||
*
|
||||
* assertSpyCalls(methodStub, 5);
|
||||
* ```
|
||||
|
||||
*
|
||||
* @typeParam Self The self type of the instance to replace a method of.
|
||||
* @typeParam Prop The property of the instance to replace.
|
||||
* @param self The instance to replace a method of.
|
||||
* @param property The property of the instance to replace.
|
||||
* @returns The stub function which replaced the original.
|
||||
*/
|
||||
export function stub<
|
||||
Self,
|
||||
Prop extends keyof Self,
|
||||
>(
|
||||
self: Self,
|
||||
property: Prop,
|
||||
): Stub<Self, GetParametersFromProp<Self, Prop>, GetReturnFromProp<Self, Prop>>;
|
||||
/**
|
||||
* Replaces an instance method with a Stub with the given implementation.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* import { stub } from "@std/testing/unstable-stub";
|
||||
* import { assertEquals } from "@std/assert";
|
||||
*
|
||||
* const obj = {
|
||||
* method(): number {
|
||||
* return Math.random();
|
||||
* },
|
||||
* };
|
||||
*
|
||||
* const methodStub = stub(obj, "method", () => 0.5);
|
||||
*
|
||||
* assertEquals(obj.method(), 0.5);
|
||||
* ```
|
||||
*
|
||||
* @typeParam Self The self type of the instance to replace a method of.
|
||||
* @typeParam Prop The property of the instance to replace.
|
||||
* @param self The instance to replace a method of.
|
||||
* @param property The property of the instance to replace.
|
||||
* @param func The fake implementation of the function.
|
||||
* @returns The stub function which replaced the original.
|
||||
*/
|
||||
export function stub<
|
||||
Self,
|
||||
Prop extends keyof Self,
|
||||
>(
|
||||
self: Self,
|
||||
property: Prop,
|
||||
func: (
|
||||
this: Self,
|
||||
...args: GetParametersFromProp<Self, Prop>
|
||||
) => GetReturnFromProp<Self, Prop>,
|
||||
): Stub<Self, GetParametersFromProp<Self, Prop>, GetReturnFromProp<Self, Prop>>;
|
||||
/**
|
||||
* Replaces an instance property setter or getter with a Stub with the given implementation.
|
||||
*
|
||||
* @example Usage
|
||||
* ```ts
|
||||
* import { assertSpyCalls } from "@std/testing/mock";
|
||||
* import { stub } from "@std/testing/unstable-stub";
|
||||
* import { assertEquals } from "@std/assert";
|
||||
*
|
||||
* const obj = {
|
||||
* prop: "foo",
|
||||
* };
|
||||
*
|
||||
* const getterStub = stub(obj, "prop", {
|
||||
* get: function () {
|
||||
* return "bar";
|
||||
* },
|
||||
* });
|
||||
*
|
||||
* assertEquals(obj.prop, "bar");
|
||||
* assertSpyCalls(getterStub.get, 1);
|
||||
* ```
|
||||
*
|
||||
* @typeParam Self The self type of the instance to replace a method of.
|
||||
* @typeParam Prop The property of the instance to replace.
|
||||
* @param self The instance to replace a method of.
|
||||
* @param property The property of the instance to replace.
|
||||
* @param descriptor The javascript property descriptor with fake implementation of the getter and setter.
|
||||
* @returns The stub with get and set properties which are spys of the setter and getter.
|
||||
*/
|
||||
export function stub<Self, Prop extends keyof Self>(
|
||||
self: Self,
|
||||
property: Prop,
|
||||
descriptor: Omit<PropertyDescriptor, "configurable">,
|
||||
):
|
||||
& Stub<
|
||||
Self,
|
||||
GetParametersFromProp<Self, Prop>,
|
||||
GetReturnFromProp<Self, Prop>
|
||||
>
|
||||
& {
|
||||
get: Spy<
|
||||
Self,
|
||||
GetParametersFromProp<Self, Prop>,
|
||||
GetReturnFromProp<Self, Prop>
|
||||
>;
|
||||
set: Spy<
|
||||
Self,
|
||||
GetParametersFromProp<Self, Prop>,
|
||||
GetReturnFromProp<Self, Prop>
|
||||
>;
|
||||
};
|
||||
export function stub<Self, Args extends unknown[], Return>(
|
||||
self: Self,
|
||||
property: keyof Self,
|
||||
descriptorOrFunction?:
|
||||
| ((this: Self, ...args: Args) => Return)
|
||||
| Omit<PropertyDescriptor, "configurable">,
|
||||
): Stub<Self, Args, Return> {
|
||||
if (
|
||||
self[property] !== undefined &&
|
||||
typeof self[property] !== "function" &&
|
||||
(descriptorOrFunction === undefined ||
|
||||
typeof descriptorOrFunction === "function")
|
||||
) {
|
||||
throw new MockError("Cannot stub: property is not an instance method");
|
||||
}
|
||||
if (isSpy(self[property])) {
|
||||
throw new MockError("Cannot stub: already spying on instance method");
|
||||
}
|
||||
if (
|
||||
descriptorOrFunction !== undefined &&
|
||||
typeof descriptorOrFunction !== "function" &&
|
||||
descriptorOrFunction.get === undefined &&
|
||||
descriptorOrFunction.set === undefined
|
||||
) {
|
||||
throw new MockError(
|
||||
"Cannot stub: neither setter nor getter is defined",
|
||||
);
|
||||
}
|
||||
|
||||
const propertyDescriptor = Object.getOwnPropertyDescriptor(self, property);
|
||||
if (propertyDescriptor && !propertyDescriptor.configurable) {
|
||||
throw new MockError("Cannot stub: non-configurable instance method");
|
||||
}
|
||||
const fake =
|
||||
descriptorOrFunction && typeof descriptorOrFunction === "function"
|
||||
? descriptorOrFunction
|
||||
: ((() => {}) as (this: Self, ...args: Args) => Return);
|
||||
|
||||
const original = self[property] as unknown as (
|
||||
this: Self,
|
||||
...args: Args
|
||||
) => Return;
|
||||
const calls: SpyCall<Self, Args, Return>[] = [];
|
||||
let restored = false;
|
||||
const stub = function (this: Self, ...args: Args): Return {
|
||||
const call: SpyCall<Self, Args, Return> = { 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<Self, Args, Return>;
|
||||
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(
|
||||
"Cannot restore: instance method already restored",
|
||||
);
|
||||
}
|
||||
if (propertyDescriptor) {
|
||||
Object.defineProperty(self, property, propertyDescriptor);
|
||||
} else {
|
||||
delete self[property];
|
||||
}
|
||||
restored = true;
|
||||
unregisterMock(stub);
|
||||
},
|
||||
},
|
||||
[Symbol.dispose]: {
|
||||
value: () => {
|
||||
stub.restore();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (descriptorOrFunction && typeof descriptorOrFunction !== "function") {
|
||||
const getterSpy = descriptorOrFunction.get
|
||||
? spy(descriptorOrFunction.get)
|
||||
: undefined;
|
||||
const setterSpy = descriptorOrFunction.set
|
||||
? spy(descriptorOrFunction.set)
|
||||
: undefined;
|
||||
|
||||
Object.defineProperty(self, property, {
|
||||
configurable: true,
|
||||
enumerable: propertyDescriptor?.enumerable ?? false,
|
||||
get: getterSpy!,
|
||||
set: setterSpy!,
|
||||
});
|
||||
Object.defineProperty(stub, "get", {
|
||||
value: getterSpy,
|
||||
enumerable: true,
|
||||
});
|
||||
Object.defineProperty(stub, "set", {
|
||||
value: setterSpy,
|
||||
enumerable: true,
|
||||
});
|
||||
} else {
|
||||
Object.defineProperty(self, property, {
|
||||
configurable: true,
|
||||
enumerable: propertyDescriptor?.enumerable ?? false,
|
||||
writable: propertyDescriptor?.writable ?? false,
|
||||
value: stub,
|
||||
});
|
||||
}
|
||||
|
||||
registerMock(stub);
|
||||
return stub;
|
||||
}
|
394
testing/unstable_stub_test.ts
Normal file
394
testing/unstable_stub_test.ts
Normal file
@ -0,0 +1,394 @@
|
||||
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
||||
import { assertEquals, assertThrows } from "@std/assert";
|
||||
import {
|
||||
assertSpyCall,
|
||||
assertSpyCallArg,
|
||||
assertSpyCalls,
|
||||
MockError,
|
||||
returnsNext,
|
||||
} from "./mock.ts";
|
||||
import { Point, type PointWithExtra } from "./_test_utils.ts";
|
||||
import { type Stub, stub } from "./unstable_stub.ts";
|
||||
|
||||
Deno.test("stub()", () => {
|
||||
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,
|
||||
"Cannot restore: instance method already restored",
|
||||
);
|
||||
assertEquals(func.restored, true);
|
||||
});
|
||||
|
||||
Deno.test("stub() works on 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,
|
||||
"Cannot restore: instance method already restored",
|
||||
);
|
||||
assertEquals(func.restored, true);
|
||||
});
|
||||
Deno.test("stub() works on getter", () => {
|
||||
const point = new Point(5, 6);
|
||||
const returns = [1, 2, 3, 4];
|
||||
const func = stub(point, "x", {
|
||||
get: function () {
|
||||
return returns.shift();
|
||||
},
|
||||
});
|
||||
|
||||
assertSpyCalls(func.get, 0);
|
||||
|
||||
assertEquals(point.x, 1);
|
||||
assertSpyCall(func.get, 0, {
|
||||
self: point,
|
||||
args: [],
|
||||
returned: 1,
|
||||
});
|
||||
assertSpyCalls(func.get, 1);
|
||||
|
||||
assertEquals(point.x, 2);
|
||||
assertSpyCall(func.get, 1, {
|
||||
self: point,
|
||||
args: [],
|
||||
returned: 2,
|
||||
});
|
||||
assertSpyCalls(func.get, 2);
|
||||
|
||||
assertEquals(func.restored, false);
|
||||
func.restore();
|
||||
assertEquals(func.restored, true);
|
||||
assertEquals(point.x, 5);
|
||||
assertThrows(
|
||||
() => func.restore(),
|
||||
MockError,
|
||||
"Cannot restore: instance method already restored",
|
||||
);
|
||||
assertEquals(func.restored, true);
|
||||
});
|
||||
Deno.test("stub() works on setter", () => {
|
||||
const point = new Point(5, 6);
|
||||
const func = stub(point, "x", {
|
||||
set: function (value: number) {
|
||||
point.y = value;
|
||||
},
|
||||
});
|
||||
|
||||
assertSpyCalls(func.set, 0);
|
||||
assertEquals(point.y, 6);
|
||||
|
||||
point.x = 10;
|
||||
|
||||
assertEquals(point.y, 10);
|
||||
assertSpyCalls(func.set, 1);
|
||||
assertSpyCallArg(func.set, 0, 0, 10);
|
||||
|
||||
point.x = 15;
|
||||
|
||||
assertEquals(point.y, 15);
|
||||
assertSpyCalls(func.set, 2);
|
||||
assertSpyCallArg(func.set, 1, 0, 15);
|
||||
|
||||
assertEquals(func.restored, false);
|
||||
func.restore();
|
||||
assertEquals(func.restored, true);
|
||||
assertThrows(
|
||||
() => func.restore(),
|
||||
MockError,
|
||||
"Cannot restore: instance method already restored",
|
||||
);
|
||||
assertEquals(func.restored, true);
|
||||
});
|
||||
Deno.test("stub() works on getter and setter", () => {
|
||||
const point = new Point(5, 6);
|
||||
const returns = [1, 2, 3, 4];
|
||||
const func = stub(point, "x", {
|
||||
get: function () {
|
||||
return returns.shift();
|
||||
},
|
||||
set: function (value: number) {
|
||||
point.y = value;
|
||||
},
|
||||
});
|
||||
|
||||
assertSpyCalls(func.set, 0);
|
||||
assertSpyCalls(func.get, 0);
|
||||
assertEquals(point.y, 6);
|
||||
assertEquals(point.x, 1);
|
||||
|
||||
point.x = 10;
|
||||
|
||||
assertEquals(point.y, 10);
|
||||
assertSpyCalls(func.set, 1);
|
||||
assertSpyCalls(func.get, 1);
|
||||
assertSpyCallArg(func.set, 0, 0, 10);
|
||||
assertSpyCall(func.get, 0, {
|
||||
self: point,
|
||||
args: [],
|
||||
returned: 1,
|
||||
});
|
||||
|
||||
point.x = 15;
|
||||
|
||||
assertEquals(point.x, 2);
|
||||
assertEquals(point.y, 15);
|
||||
assertSpyCalls(func.set, 2);
|
||||
assertSpyCalls(func.get, 2);
|
||||
assertSpyCallArg(func.set, 1, 0, 15);
|
||||
assertSpyCall(func.get, 1, {
|
||||
self: point,
|
||||
args: [],
|
||||
returned: 2,
|
||||
});
|
||||
|
||||
assertEquals(func.restored, false);
|
||||
func.restore();
|
||||
assertEquals(func.restored, true);
|
||||
assertThrows(
|
||||
() => func.restore(),
|
||||
MockError,
|
||||
"Cannot restore: instance method already restored",
|
||||
);
|
||||
assertEquals(point.x, 5);
|
||||
assertEquals(func.restored, true);
|
||||
});
|
||||
Deno.test("stub() supports explicit resource management", () => {
|
||||
const point = new Point(2, 3);
|
||||
const returns = [1, "b", 2, "d"];
|
||||
let funcRef: Stub<Point> | null = null;
|
||||
{
|
||||
using func = stub(point, "action", () => returns.shift());
|
||||
funcRef = func;
|
||||
|
||||
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);
|
||||
}
|
||||
if (funcRef) {
|
||||
assertEquals(funcRef.restored, true);
|
||||
assertEquals(point.action, Point.prototype.action);
|
||||
assertThrows(
|
||||
() => {
|
||||
if (funcRef) funcRef.restore();
|
||||
},
|
||||
MockError,
|
||||
"Cannot restore: instance method already restored",
|
||||
);
|
||||
assertEquals(funcRef.restored, true);
|
||||
}
|
||||
});
|
||||
Deno.test("stub() handles non existent function", () => {
|
||||
const point = new Point(2, 3);
|
||||
const castPoint = point as PointWithExtra;
|
||||
let i = 0;
|
||||
const func = stub(castPoint, "nonExistent", () => {
|
||||
i++;
|
||||
return i;
|
||||
});
|
||||
|
||||
assertSpyCalls(func, 0);
|
||||
|
||||
assertEquals(func.call(castPoint), 1);
|
||||
assertSpyCall(func, 0, {
|
||||
self: castPoint,
|
||||
args: [],
|
||||
returned: 1,
|
||||
});
|
||||
assertSpyCalls(func, 1);
|
||||
|
||||
assertEquals(castPoint.nonExistent(), 2);
|
||||
assertSpyCall(func, 1, {
|
||||
self: castPoint,
|
||||
args: [],
|
||||
returned: 2,
|
||||
});
|
||||
assertSpyCalls(func, 2);
|
||||
|
||||
assertEquals(func.original, undefined);
|
||||
assertEquals(castPoint.nonExistent, func);
|
||||
|
||||
assertEquals(func.restored, false);
|
||||
func.restore();
|
||||
assertEquals(func.restored, true);
|
||||
assertEquals(castPoint.nonExistent, undefined);
|
||||
assertThrows(
|
||||
() => func.restore(),
|
||||
MockError,
|
||||
"Cannot restore: instance method already restored",
|
||||
);
|
||||
assertEquals(func.restored, true);
|
||||
});
|
||||
|
||||
// This doesn't test any runtime code, only if the TypeScript types are correct.
|
||||
Deno.test("stub() correctly handles types", () => {
|
||||
// @ts-expect-error Stubbing with incorrect argument types should cause a type error
|
||||
stub(new Point(2, 3), "explicitTypes", (_x: string, _y: number) => true);
|
||||
|
||||
// @ts-expect-error Stubbing with an incorrect return type should cause a type error
|
||||
stub(new Point(2, 3), "explicitTypes", () => "string");
|
||||
|
||||
// Stubbing without argument types infers them from the real function
|
||||
stub(new Point(2, 3), "explicitTypes", (_x, _y) => {
|
||||
// `toExponential()` only exists on `number`, so this will error if _x is not a number
|
||||
_x.toExponential();
|
||||
// `toLowerCase()` only exists on `string`, so this will error if _y is not a string
|
||||
_y.toLowerCase();
|
||||
return true;
|
||||
});
|
||||
|
||||
// Stubbing with returnsNext() should not give any type errors
|
||||
stub(new Point(2, 3), "explicitTypes", returnsNext([true, false, true]));
|
||||
|
||||
// Stubbing without argument types should not cause any type errors:
|
||||
const point2 = new Point(2, 3);
|
||||
const explicitTypesFunc = stub(point2, "explicitTypes", () => true);
|
||||
|
||||
// Check if the returned type is correct:
|
||||
assertThrows(() => {
|
||||
assertSpyCall(explicitTypesFunc, 0, {
|
||||
// @ts-expect-error Test if passing incorrect argument types causes an error
|
||||
args: ["not a number", "string"],
|
||||
// @ts-expect-error Test if passing incorrect return type causes an error
|
||||
returned: "not a boolean",
|
||||
});
|
||||
});
|
||||
|
||||
// Calling assertSpyCall with the correct types should not cause any type errors
|
||||
point2.explicitTypes(1, "hello");
|
||||
assertSpyCall(explicitTypesFunc, 0, {
|
||||
args: [1, "hello"],
|
||||
returned: true,
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("stub() works with throwing fake implementation", () => {
|
||||
const obj = {
|
||||
fn() {
|
||||
throw new Error("failed");
|
||||
},
|
||||
};
|
||||
const stubFn = stub(obj, "fn", () => {
|
||||
throw new Error("failed");
|
||||
});
|
||||
assertThrows(() => obj.fn(), Error, "failed");
|
||||
assertSpyCall(stubFn, 0, {
|
||||
self: obj,
|
||||
args: [],
|
||||
error: { Class: Error, msgIncludes: "failed" },
|
||||
});
|
||||
});
|
||||
|
||||
Deno.test("stub() throws when the property is not a method", () => {
|
||||
const obj = { fn: 1 };
|
||||
assertThrows(
|
||||
// deno-lint-ignore no-explicit-any
|
||||
() => stub(obj as any, "fn"),
|
||||
MockError,
|
||||
"Cannot stub: property is not an instance method",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("stub() throws when try stubbing already stubbed method", () => {
|
||||
const obj = { fn() {} };
|
||||
stub(obj, "fn");
|
||||
assertThrows(
|
||||
() => stub(obj, "fn"),
|
||||
MockError,
|
||||
"Cannot stub: already spying on instance method",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("stub() throws when neither setter not getter is defined", () => {
|
||||
const obj = { prop: "foo" };
|
||||
|
||||
assertThrows(
|
||||
() => stub(obj, "prop", {}),
|
||||
MockError,
|
||||
"Cannot stub: neither setter nor getter is defined",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("stub() throws then the property is not configurable", () => {
|
||||
const obj = { fn() {} };
|
||||
Object.defineProperty(obj, "fn", { configurable: false });
|
||||
assertThrows(
|
||||
() => stub(obj, "fn"),
|
||||
MockError,
|
||||
"Cannot stub: non-configurable instance method",
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue
Block a user