Spec-Zone.ru › Deno 2

Моки и тестовые дублёры

Модульные тесты наиболее полезны, когда проверяют за раз что-то одно. Реальные зависимости, такие как базы данных, HTTP API или системные часы, делают тесты медленными, нестабильными и сложными в настройке. Тестовый дублёр — это контролируемая замена такой зависимости. Он позволяет проверить, как ваш код взаимодействует с зависимостью, не неся затрат на использование настоящей. В стандартной библиотеке Deno есть всё необходимое в @std/testing/mock и @std/testing/time.

Шпионы

Шпион оборачивает функцию и записывает каждый вызов: сколько раз она была запущена и какие аргументы получила. Шпионы не меняют поведение функции, поэтому это самый ненавязчивый вид тестового дублёра.

Здесь мы тестируем функцию saveUser(), которая передаёт пользователя объекту базы данных. Вместо настоящей базы данных мы передаём объект, метод save которого является шпионом:

import { assertEquals } from "jsr:@std/assert";
import { assertSpyCall, assertSpyCalls, spy } from "jsr:@std/testing/mock";

interface User {
  name: string;
}

interface Database {
  save: (user: User) => Promise<User & { id: number }>;
}

// Function to test
function saveUser(
  user: User,
  database: Database,
): Promise<User & { id: number }> {
  return database.save(user);
}

Deno.test("saveUser calls database.save", async () => {
  // A stand-in database whose save method is wrapped in a spy
  const mockDatabase = {
    save: spy((user: User) => Promise.resolve({ id: 1, ...user })),
  };

  const user: User = { name: "Test User" };
  const result = await saveUser(user, mockDatabase);

  // The save method was called exactly once
  assertSpyCalls(mockDatabase.save, 1);

  // The first call (index 0) received our user object
  assertSpyCall(mockDatabase.save, 0, { args: [user] });

  assertEquals(result, { id: 1, name: "Test User" });
});

assertSpyCalls проверяет общее число вызовов, а assertSpyCall исследует отдельный вызов по индексу: его аргументы и, при необходимости, возвращённое значение. Мы проверили всё взаимодействие, не настраивая и не очищая состояние базы данных.

Можно также следить за существующим методом объекта. Шпионы методов автоматически удаляются, поэтому объявляйте их с ключевым словом using: исходный метод будет автоматически восстановлен по завершении теста. Очистка с помощью try/finally не требуется:

import { assertEquals } from "jsr:@std/assert";
import { assertSpyCalls, spy } from "jsr:@std/testing/mock";

Deno.test("using disposable spies", () => {
  const calculator = {
    add: (a: number, b: number) => a + b,
  };

  // Restored automatically when it goes out of scope
  using addSpy = spy(calculator, "add");

  const sum = calculator.add(3, 4);
  assertEquals(sum, 7);
  assertSpyCalls(addSpy, 1);
  assertEquals(addSpy.calls[0].args, [3, 4]);
});

Если вы не можете использовать using, вызовите addSpy.restore() в блоке finally. В любом случае восстановление шпионов не позволяет состоянию переходить из одного теста в другой.

Стабы

Стаб идёт на шаг дальше шпиона: он полностью заменяет исходную реализацию. Используйте стабы, чтобы возвращать заранее заданные значения, имитировать ошибки или не позволять тестам обращаться к внешним сервисам. Стаб также записывает вызовы, поэтому к нему применимы все проверки для шпионов.

import { assertEquals } from "jsr:@std/assert";
import { stub } from "jsr:@std/testing/mock";

// Wrap dependencies in an object so they can be stubbed from tests
const deps = {
  getUserName(_id: number): string {
    // In a real app, this might call a database
    return "Original User";
  },
};

// Function under test
function greetUser(id: number): string {
  const name = deps.getUserName(id);
  return `Hello, ${name}!`;
}

Deno.test("greetUser with stubbed getUserName", () => {
  // Replace the implementation with a controlled one
  using getUserNameStub = stub(deps, "getUserName", () => "Test User");

  assertEquals(greetUser(123), "Hello, Test User!");
  assertEquals(getUserNameStub.calls.length, 1);
});

Как и шпионы методов, стабы автоматически удаляются. Ключевое слово using восстанавливает исходную реализацию, когда стаб выходит из области видимости, а restore() доступен, если нужно управлять этим вручную.

Возврат последовательности значений

Если стаб должен возвращать разные значения при каждом вызове, передайте returnsNext со значениями в нужном порядке. Это удобно для имитации отсутствующей записи, повторной попытки или любого сценария, зависящего от номера вызова:

import { assertEquals, assertThrows } from "jsr:@std/assert";
import { assertSpyCalls, returnsNext, stub } from "jsr:@std/testing/mock";

type User = { id: number; name: string };

const database = {
  getUserById(id: number): User | undefined {
    return { id, name: "Ada Lovelace" };
  },
};

function findOrThrow(id: number): User {
  const user = database.getUserById(id);
  if (!user) {
    throw new Error("User not found");
  }
  return user;
}

Deno.test("findOrThrow throws when the user was not found", () => {
  // The stub returns undefined on its first (and only) call
  using dbStub = stub(database, "getUserById", returnsNext([undefined]));

  assertThrows(() => findOrThrow(1), Error, "User not found");
  assertSpyCalls(dbStub, 1);
});

Deno.test("stub with multiple return values", () => {
  const dataService = { fetchData: () => "original data" };

  using fetchDataStub = stub(
    dataService,
    "fetchData",
    returnsNext(["first result", "second result"]),
  );

  assertEquals(dataService.fetchData(), "first result");
  assertEquals(dataService.fetchData(), "second result");
  assertSpyCalls(fetchDataStub, 2);
});

Шпион, стаб или мок?

Эти понятия пересекаются, но различие простое:

  • Шпион наблюдает. Он записывает вызовы, пока выполняется реальное поведение.
  • Стаб заменяет. Он записывает вызовы и подставляет контролируемую реализацию.
  • Мок — это общее название любого тестового дублёра. На практике «объект-мок» обычно представляет собой созданную вручную замену (например, mockDatabase выше), методы которой являются шпионами или стабами; после выполнения тестируемого кода для них выполняются проверки.

Всё необходимое для всех трёх вариантов есть в jsr:@std/testing/mock.

Имитация времени

Код, который считывает время или устанавливает таймеры, выдаёт разные результаты в зависимости от момента запуска. FakeTime из jsr:@std/testing/time заменяет Date, setTimeout, setInterval и другие функции управляемыми версиями, так что можно зафиксировать текущую дату и мгновенно перемотать время с помощью tick() вместо ожидания реальных тайм-аутов:

import { assertEquals } from "jsr:@std/assert";
import { FakeTime } from "jsr:@std/testing/time";

// Function that depends on the current time
function isWeekend(): boolean {
  const day = new Date().getDay();
  return day === 0 || day === 6; // 0 is Sunday, 6 is Saturday
}

// Function that works with timeouts
function delayedGreeting(callback: (message: string) => void): void {
  setTimeout(() => callback("Hello after delay"), 1000);
}

Deno.test("time-dependent tests", () => {
  // Start the fake clock on a known Monday; restored automatically
  using time = new FakeTime(new Date("2023-05-01T12:00:00Z"));

  assertEquals(isWeekend(), false);

  // Advance 5 days to Saturday
  time.tick(5 * 24 * 60 * 60 * 1000);
  assertEquals(isWeekend(), true);

  // Timers fire as soon as the fake clock passes them
  let greeting = "";
  delayedGreeting((message) => {
    greeting = message;
  });
  time.tick(1000);
  assertEquals(greeting, "Hello after delay");
});

Имитация времени особенно полезна для календарных функций, таких как планирование и истечение срока действия, для кода, использующего тайм-ауты или интервалы, например опроса и устранения дребезга, а также для всего, где ожидание реальных часов сделало бы тесты медленными или нестабильными. FakeTime автоматически удаляется, поэтому using восстанавливает реальные часы по завершении теста.

Выбор подходящего тестового дублёра

Выбирайте самый простой дублёр, который справится с задачей. Если нужно лишь проверить, что взаимодействие произошло, используйте шпион. Если реальная реализация медленная, недетерминированная или внешняя, замените её стабом. Если зависимостью являются часы, используйте имитацию времени вместо того, чтобы вручную подменять Date. Какой бы вариант вы ни выбрали, создавайте стаб на границе интерфейса (объект deps, слой базы данных), а не глубоко внутри деталей реализации; поручите using восстановление исходного состояния, чтобы один тест не влиял на следующий.

Что дальше

  • Тестирование в Deno: запуск тестов, проверки и покрытие кода
  • Тестирование со снимками: сравнение вывода с сохранёнными снимками
  • Документация API @std/testing: подробное описание каждой утилиты
  • Другие примеры: запускаемые учебные материалы и примеры

Последнее обновление: 12 июня 2026 г.

© 2018–2025 the Deno authors
Licensed under the MIT License.
https://docs.deno.com/runtime/test/mocking

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API