跳至主要内容

模拟

测试间谍是函数替身,用于断言函数的内部行为是否符合预期。方法上的测试间谍保留原始行为,但允许您测试方法的调用方式及其返回值。测试存根是测试间谍的扩展,它还替换了原始方法的行为。

间谍

假设我们有两个函数,squaremultiply,如果我们想断言 multiply 函数在 square 函数执行期间被调用,我们需要一种方法来监视 multiply 函数。使用间谍可以实现这一点,一种方法是让 square 函数将 multiply 作为参数。

// https://deno.land/std/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);
}

这样,我们可以在应用程序代码中调用 square(multiply, value),或者在测试代码中将间谍函数包装在 multiply 函数周围并调用 square(multiplySpy, value)

// https://deno.land/std/testing/mock_examples/parameter_injection_test.ts
import {
assertSpyCall,
assertSpyCalls,
spy,
} from "https://deno.land/[email protected]/testing/mock.ts";
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
import {
multiply,
square,
} from "https://deno.land/[email protected]/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);
});

如果您不想为了测试目的而添加额外的参数,可以使用 spy 来包装对象上的方法。在以下示例中,导出的 _internals 对象具有我们想要作为方法调用的 multiply 函数,而 square 函数调用 _internals.multiply 而不是 multiply

// https://deno.land/std/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 };

这样,我们就可以在应用程序代码和测试代码中都调用 square(value)。然后在测试代码中对 _internals 对象上的 multiply 方法进行 spy,以便能够 spy square 函数如何调用 multiply 函数。

// https://deno.land/std/testing/mock_examples/internals_injection_test.ts
import {
assertSpyCall,
assertSpyCalls,
spy,
} from "https://deno.land/[email protected]/testing/mock.ts";
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
import {
_internals,
square,
} from "https://deno.land/[email protected]/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);
});

您可能已经注意到这两个示例之间的一个区别,那就是在第二个示例中,我们调用了 multiplySpy 函数上的 restore 方法。这需要从 _internals 对象的 multiply 方法中移除 spy 包装器。restore 方法在 finally 块中调用,以确保无论 try 块中的断言是否成功,它都会被恢复。在第一个示例中不需要调用 restore 方法,因为 multiply 函数没有像第二个示例中的 _internals 对象那样被修改。

存根

假设我们有两个函数,randomMultiplerandomInt,如果我们想断言 randomInt 在执行 randomMultiple 期间被调用,我们需要一种方法来 spy randomInt 函数。这可以使用前面提到的任何一种 spy 技术来完成。为了能够验证 randomMultiple 函数返回我们期望它返回的值,对于 randomInt 返回的值,最简单的方法是将 randomInt 函数的行为替换为更可预测的行为。

您可以使用第一种 spy 技术来做到这一点,但这将需要向 randomMultiple 函数添加一个 randomInt 参数。

您也可以使用第二种 spy 技术来做到这一点,但您的断言将不可预测,因为 randomInt 函数返回随机值。

假设我们想验证它对负随机整数和正随机整数都返回正确的值。我们可以使用存根轻松地做到这一点。下面的示例类似于第二种 spy 技术示例,但我们不是将调用传递给原始的 randomInt 函数,而是将 randomInt 替换为一个返回预定义值的函数。

// https://deno.land/std/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 };

模拟模块包含一些辅助函数,可以轻松创建常见的存根。returnsNext 函数接受一个值数组,我们希望它在连续调用时返回这些值。

// https://deno.land/std/testing/mock_examples/random_test.ts
import {
assertSpyCall,
assertSpyCalls,
returnsNext,
stub,
} from "https://deno.land/[email protected]/testing/mock.ts";
import { assertEquals } from "https://deno.land/[email protected]/assert/mod.ts";
import {
_internals,
randomMultiple,
} from "https://deno.land/[email protected]/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);
});

模拟时间

假设我们有一个具有基于时间的行为的函数,我们想对其进行测试。使用真实时间,这会导致测试花费的时间比应有的时间长得多。如果你模拟时间,你可以模拟你的函数如何随时间推移而表现,从任何时间点开始。以下是一个示例,我们希望测试回调函数是否每秒钟被调用一次。

// https://deno.land/std/testing/mock_examples/interval.ts
export function secondInterval(cb: () => void): number {
return setInterval(cb, 1000);
}

使用 FakeTime,我们可以做到这一点。当 FakeTime 实例被创建时,它会从真实时间分离。DatesetTimeoutclearTimeoutsetIntervalclearInterval 全局变量被替换为使用模拟时间的版本,直到真实时间恢复。你可以使用 FakeTime 实例上的 tick 方法来控制时间如何向前推进。

// https://deno.land/std/testing/mock_examples/interval_test.ts
import {
assertSpyCalls,
spy,
} from "https://deno.land/[email protected]/testing/mock.ts";
import { FakeTime } from "https://deno.land/[email protected]/testing/time.ts";
import { secondInterval } from "https://deno.land/[email protected]/testing/mock_examples/interval.ts";

Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
const time = new FakeTime();

try {
const cb = spy();
const intervalId = secondInterval(cb);
assertSpyCalls(cb, 0);
time.tick(500);
assertSpyCalls(cb, 0);
time.tick(500);
assertSpyCalls(cb, 1);
time.tick(3500);
assertSpyCalls(cb, 4);

clearInterval(intervalId);
time.tick(1000);
assertSpyCalls(cb, 4);
} finally {
time.restore();
}
});