模拟
测试间谍是函数替身,用于断言函数的内部行为是否符合预期。方法上的测试间谍保留原始行为,但允许您测试方法的调用方式及其返回值。测试存根是测试间谍的扩展,它还替换了原始方法的行为。
间谍
假设我们有两个函数,square
和 multiply
,如果我们想断言 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
对象那样被修改。
存根
假设我们有两个函数,randomMultiple
和 randomInt
,如果我们想断言 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
实例被创建时,它会从真实时间分离。Date
、setTimeout
、clearTimeout
、setInterval
和 clearInterval
全局变量被替换为使用模拟时间的版本,直到真实时间恢复。你可以使用 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();
}
});