跳到主要内容

Mocking

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

Spying

假设我们有两个函数,squaremultiply,如果我们想要断言在执行 square 函数时 multiply 函数被调用,我们需要一种方法来监视 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),或者在测试代码中包装一个监视函数,然后调用 square(multiplySpy, value)

// https://deno.land/std/testing/mock_examples/parameter_injection_test.ts
import {
assertSpyCall,
assertSpyCalls,
spy,
} from "https://deno.land/std@0.208.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.208.0/assert/mod.ts";
import {
multiply,
square,
} from "https://deno.land/std@0.208.0/testing/mock_examples/parameter_injection.ts";

Deno.test("square calls multiply and returns results", () => {
const multiplySpy = spy(multiply);

assertEquals(square(multiplySpy, 5), 25);

// 断言 multiplySpy 至少被调用一次,以及有关第一次调用的详细信息。
assertSpyCall(multiplySpy, 0, {
args: [5, 5],
returned: 25,
});

// 断言 multiplySpy 只被调用一次。
assertSpyCalls(multiplySpy, 1);
});

如果你不想为测试目的添加额外的参数,你可以使用监视函数来包装对象上的方法。在以下示例中,导出的 _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 方法,以便监视 square 函数如何调用 multiply 函数。

// https://deno.land/std/testing/mock_examples/internals_injection_test.ts
import {
assertSpyCall,
assertSpyCalls,
spy,
} from "https://deno.land/std@0.208.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.208.0/assert/mod.ts";
import {
_internals,
square,
} from "https://deno.land/std@0.208.0/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 {
// 解除 _internals 对象上的 multiply 方法的监视包装
multiplySpy.restore();
}

// 断言 multiplySpy 至少被调用一次,以及有关第一次调用的详细信息。
assertSpyCall(multiplySpy, 0, {
args: [5, 5],
returned: 25,
});

// 断言 multiplySpy 只被调用一次。
assertSpyCalls(multiplySpy, 1);
});

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

存根

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

你可以使用第一个监视技术来做到这一点,但这需要向 randomMultiple 函数添加一个 randomInt 参数。

你也可以使用第二个监视技术来做到这一点,但由于 randomInt 函数返回随机值,你的断言可能不够可预测。

假设我们希望验证它对负数和正数随机整数都返回正确的值。我们可以使用存根轻松实现这一点。下面的示例类似于第二个监视技术示例,但不是将调用传递给原始的 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 random

Multiple(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/std@0.208.0/testing/mock.ts";
import { assertEquals } from "https://deno.land/std@0.208.0/assert/mod.ts";
import {
_internals,
randomMultiple,
} from "https://deno.land/std@0.208.0/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 {
// 解除 _internals 对象上的 randomInt 方法的监视包装
randomIntStub.restore();
}

// 断言 randomIntStub 至少被调用一次,以及有关第一次调用的详细信息。
assertSpyCall(randomIntStub, 0, {
args: [-10, 10],
returned: -3,
});
// 断言 randomIntStub 至少被调用两次,以及有关第二次调用的详细信息。
assertSpyCall(randomIntStub, 1, {
args: [-10, 10],
returned: 3,
});

// 断言 randomIntStub 只被调用两次。
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/std@0.208.0/testing/mock.ts";
import { FakeTime } from "https://deno.land/std@0.208.0/testing/time.ts";
import { secondInterval } from "https://deno.land/std@0.208.0/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();
}
});