在本页

模拟

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

间谍 跳转到标题

假设我们有两个函数,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),或者在测试代码中将间谍函数包装在 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);
});

如果您不想仅出于测试目的添加额外的参数,可以使用间谍来包装对象上的方法。在下面的示例中,导出的 _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/[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 方法中删除间谍包装器。restore 方法在 finally 块中调用,以确保无论 try 块中的断言是否成功,它都会被恢复。在第一个示例中不需要调用 restore 方法,因为 multiply 函数没有像第二个示例中的 _internals 对象那样被修改。

存根 跳转到标题

假设我们有两个函数,randomMultiplerandomInt,如果我们想断言在执行 randomMultiple 函数期间调用了 randomInt 函数,我们需要一种方法来监视 randomInt 函数。这可以使用前面提到的任何一种间谍技术来完成。为了能够验证 randomMultiple 函数是否返回了我们期望它返回的值(对于 randomInt 返回的值),最简单的方法是将 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 randomMultiple(value: number): number {
  return value * _internals.randomInt(-10, 10);
}

export const _internals = { randomInt };

mock 模块包含一些辅助函数,可以轻松创建常见的存根。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();
  }
});