Skip to content

Мокирование необходимо для тестирования, позволяя заменять зависимости контролируемыми реализациями. Bun предоставляет комплексные возможности мокирования, включая мок-функции, шпионы и моки модулей.

Базовые мок-функции

Создавайте моки с помощью функции mock.

ts
import { test, expect, mock } from "bun:test";

const random = mock(() => Math.random());

test("random", () => {
  const val = random();
  expect(val).toBeGreaterThan(0);
  expect(random).toHaveBeenCalled();
  expect(random).toHaveBeenCalledTimes(1);
});

Совместимость с Jest

В качестве альтернативы вы можете использовать функцию jest.fn(), как в Jest. Она ведёт себя идентично.

ts
import { test, expect, jest } from "bun:test";

const random = jest.fn(() => Math.random());

test("random", () => {
  const val = random();
  expect(val).toBeGreaterThan(0);
  expect(random).toHaveBeenCalled();
  expect(random).toHaveBeenCalledTimes(1);
});

Свойства мок-функций

Результат mock() — это новая функция, украшенная некоторыми дополнительными свойствами.

ts
import { mock } from "bun:test";

const random = mock((multiplier: number) => multiplier * Math.random());

random(2);
random(10);

random.mock.calls;
// [[ 2 ], [ 10 ]]

random.mock.results;
//  [
//    { type: "return", value: 0.6533907460954099 },
//    { type: "return", value: 0.6452713933037312 }
//  ]

Доступные свойства и методы

Следующие свойства и методы реализованы в мок-функциях:

Свойство/МетодОписание
mockFn.getMockName()Возвращает имя мока
mockFn.mock.callsМассив аргументов вызова для каждого вызова
mockFn.mock.resultsМассив возвращаемых значений для каждого вызова
mockFn.mock.instancesМассив контекстов this для каждого вызова
mockFn.mock.contextsМассив контекстов this для каждого вызова
mockFn.mock.lastCallАргументы последнего вызова
mockFn.mockClear()Очищает историю вызовов
mockFn.mockReset()Очищает историю вызовов и удаляет реализацию
mockFn.mockRestore()Восстанавливает оригинальную реализацию
mockFn.mockImplementation(fn)Устанавливает новую реализацию
mockFn.mockImplementationOnce(fn)Устанавливает реализацию только для следующего вызова
mockFn.mockName(name)Устанавливает имя мока
mockFn.mockReturnThis()Устанавливает возвращаемое значение в this
mockFn.mockReturnValue(value)Устанавливает возвращаемое значение
mockFn.mockReturnValueOnce(value)Устанавливает возвращаемое значение только для следующего вызова
mockFn.mockResolvedValue(value)Устанавливает значение разрешённого Promise
mockFn.mockResolvedValueOnce(value)Устанавливает разрешённый Promise только для следующего вызова
mockFn.mockRejectedValue(value)Устанавливает значение отклонённого Promise
mockFn.mockRejectedValueOnce(value)Устанавливает отклонённый Promise только для следующего вызова
mockFn.withImplementation(fn, callback)Временно изменяет реализацию

Практические примеры

Использование базового мока

ts
import { test, expect, mock } from "bun:test";

test("поведение мок-функции", () => {
  const mockFn = mock((x: number) => x * 2);

  // Вызвать мок
  const result1 = mockFn(5);
  const result2 = mockFn(10);

  // Проверить вызовы
  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith(5);
  expect(mockFn).toHaveBeenLastCalledWith(10);

  // Проверить результаты
  expect(result1).toBe(10);
  expect(result2).toBe(20);

  // Проверить историю вызовов
  expect(mockFn.mock.calls).toEqual([[5], [10]]);
  expect(mockFn.mock.results).toEqual([
    { type: "return", value: 10 },
    { type: "return", value: 20 },
  ]);
});

Динамические реализации моков

ts
import { test, expect, mock } from "bun:test";

test("динамические реализации моков", () => {
  const mockFn = mock();

  // Установить различные реализации
  mockFn.mockImplementationOnce(() => "first");
  mockFn.mockImplementationOnce(() => "second");
  mockFn.mockImplementation(() => "default");

  expect(mockFn()).toBe("first");
  expect(mockFn()).toBe("second");
  expect(mockFn()).toBe("default");
  expect(mockFn()).toBe("default"); // Использует реализацию по умолчанию
});

Асинхронные моки

ts
import { test, expect, mock } from "bun:test";

test("асинхронные мок-функции", async () => {
  const asyncMock = mock();

  // Мок разрешённых значений
  asyncMock.mockResolvedValueOnce("first result");
  asyncMock.mockResolvedValue("default result");

  expect(await asyncMock()).toBe("first result");
  expect(await asyncMock()).toBe("default result");

  // Мок отклонённых значений
  const rejectMock = mock();
  rejectMock.mockRejectedValue(new Error("Mock error"));

  await expect(rejectMock()).rejects.toThrow("Mock error");
});

Шпионы с spyOn()

Можно отслеживать вызовы функции, не заменяя её моком. Используйте spyOn() для создания шпиона; эти шпионы могут быть переданы в .toHaveBeenCalled() и .toHaveBeenCalledTimes().

ts
import { test, expect, spyOn } from "bun:test";

const ringo = {
  name: "Ringo",
  sayHi() {
    console.log(`Hello I'm ${this.name}`);
  },
};

const spy = spyOn(ringo, "sayHi");

test("spyon", () => {
  expect(spy).toHaveBeenCalledTimes(0);
  ringo.sayHi();
  expect(spy).toHaveBeenCalledTimes(1);
});

Продвинутое использование шпионов

ts
import { test, expect, spyOn, afterEach } from "bun:test";

class UserService {
  async getUser(id: string) {
    // Оригинальная реализация
    return { id, name: `User ${id}` };
  }

  async saveUser(user: any) {
    // Оригинальная реализация
    return { ...user, saved: true };
  }
}

const userService = new UserService();

afterEach(() => {
  // Восстановить все шпионы после каждого теста
  jest.restoreAllMocks();
});

test("шпионаж за методами сервиса", async () => {
  // Шпион без изменения реализации
  const getUserSpy = spyOn(userService, "getUser");
  const saveUserSpy = spyOn(userService, "saveUser");

  // Использовать сервис обычным образом
  const user = await userService.getUser("123");
  await userService.saveUser(user);

  // Проверить вызовы
  expect(getUserSpy).toHaveBeenCalledWith("123");
  expect(saveUserSpy).toHaveBeenCalledWith(user);
});

test("шпион с мок-реализацией", async () => {
  // Шпион и переопределение реализации
  const getUserSpy = spyOn(userService, "getUser").mockResolvedValue({
    id: "123",
    name: "Mocked User",
  });

  const result = await userService.getUser("123");

  expect(result.name).toBe("Mocked User");
  expect(getUserSpy).toHaveBeenCalledWith("123");
});

Моки модулей с mock.module()

Мок модулей позволяет переопределять поведение модуля. Используйте mock.module(path: string, callback: () => Object) для мока модуля.

ts
import { test, expect, mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});

test("mock.module", async () => {
  const esm = await import("./module");
  expect(esm.foo).toBe("bar");

  const cjs = require("./module");
  expect(cjs.foo).toBe("bar");
});

Как и остальная часть Bun, моки модулей поддерживают как import, так и require.

Переопределение уже импортированных модулей

Если вам нужно переопределить модуль, который уже был импортирован, вам не нужно делать ничего особенного. Просто вызовите mock.module(), и модуль будет переопределён.

ts
import { test, expect, mock } from "bun:test";

// Модуль, который мы собираемся мокать, здесь:
import { foo } from "./module";

test("mock.module", async () => {
  const cjs = require("./module");
  expect(foo).toBe("bar");
  expect(cjs.foo).toBe("bar");

  // Мы обновляем его здесь:
  mock.module("./module", () => {
    return {
      foo: "baz",
    };
  });

  // И живые привязки обновлены.
  expect(foo).toBe("baz");

  // Модуль также обновлён для CJS.
  expect(cjs.foo).toBe("baz");
});

Хойстинг и предварительная загрузка

Если вам нужно убедиться, что модуль замокан до его импорта, вы должны использовать --preload для загрузки ваших моков перед запуском тестов.

ts
import { mock } from "bun:test";

mock.module("./module", () => {
  return {
    foo: "bar",
  };
});
bash
bun test --preload ./my-preload

Для упрощения вашей жизни вы можете добавить preload в ваш bunfig.toml:

toml
[test]
# Загрузить эти модули перед запуском тестов.
preload = ["./my-preload"]

Лучшие практики моков модулей

Когда использовать preload

Что произойдёт, если я замокаю модуль, который уже был импортирован?

Если вы замокаете модуль, который уже был импортирован, модуль будет обновлён в кэше модулей. Это означает, что любые модули, импортирующие этот модуль, получат замоканную версию, НО оригинальный модуль всё ещё будет вычислен. Это означает, что любые побочные эффекты оригинального модуля всё ещё произойдут.

Если вы хотите предотвратить вычисление оригинального модуля, вы должны использовать --preload для загрузки ваших моков перед запуском тестов.

Практические примеры моков модулей

ts
import { test, expect, mock, beforeEach } from "bun:test";

// Мок модуля API клиента
mock.module("./api-client", () => ({
  fetchUser: mock(async (id: string) => ({ id, name: `User ${id}` })),
  createUser: mock(async (user: any) => ({ ...user, id: "new-id" })),
  updateUser: mock(async (id: string, user: any) => ({ ...user, id })),
}));

test("сервис пользователей с замоканым API", async () => {
  const { fetchUser } = await import("./api-client");
  const { UserService } = await import("./user-service");

  const userService = new UserService();
  const user = await userService.getUser("123");

  expect(fetchUser).toHaveBeenCalledWith("123");
  expect(user.name).toBe("User 123");
});

Мок внешних зависимостей

ts
import { test, expect, mock } from "bun:test";

// Мок внешней библиотеки базы данных
mock.module("pg", () => ({
  Client: mock(function () {
    return {
      connect: mock(async () => {}),
      query: mock(async (sql: string) => ({
        rows: [{ id: 1, name: "Test User" }],
      })),
      end: mock(async () => {}),
    };
  }),
}));

test("операции с базой данных", async () => {
  const { Database } = await import("./database");
  const db = new Database();

  const users = await db.getUsers();
  expect(users).toHaveLength(1);
  expect(users[0].name).toBe("Test User");
});

Глобальные мок-функции

Очистка всех моков

Сбросьте состояние всех мок-функций (вызовы, результаты и т.д.) без восстановления их оригинальной реализации:

ts
import { expect, mock, test } from "bun:test";

const random1 = mock(() => Math.random());
const random2 = mock(() => Math.random());

test("очистка всех моков", () => {
  random1();
  random2();

  expect(random1).toHaveBeenCalledTimes(1);
  expect(random2).toHaveBeenCalledTimes(1);

  mock.clearAllMocks();

  expect(random1).toHaveBeenCalledTimes(0);
  expect(random2).toHaveBeenCalledTimes(0);

  // Примечание: реализации сохранены
  expect(typeof random1()).toBe("number");
  expect(typeof random2()).toBe("number");
});

Это сбрасывает свойства .mock.calls, .mock.instances, .mock.contexts и .mock.results всех моков, но в отличие от mock.restore(), это не восстанавливает оригинальную реализацию.

Восстановление всех моков

Вместо ручного восстановления каждого мока по отдельности с mockFn.mockRestore(), восстановите все моки одной командой, вызвав mock.restore(). Это не сбрасывает значение модулей, переопределённых с mock.module().

ts
import { expect, mock, spyOn, test } from "bun:test";

import * as fooModule from "./foo.ts";
import * as barModule from "./bar.ts";
import * as bazModule from "./baz.ts";

test("foo, bar, baz", () => {
  const fooSpy = spyOn(fooModule, "foo");
  const barSpy = spyOn(barModule, "bar");
  const bazSpy = spyOn(bazModule, "baz");

  // Оригинальные значения
  expect(fooSpy).toBe("foo");
  expect(barSpy).toBe("bar");
  expect(bazSpy).toBe("baz");

  // Мок реализации
  fooSpy.mockImplementation(() => 42);
  barSpy.mockImplementation(() => 43);
  bazSpy.mockImplementation(() => 44);

  expect(fooSpy()).toBe(42);
  expect(barSpy()).toBe(43);
  expect(bazSpy()).toBe(44);

  // Восстановить все
  mock.restore();

  expect(fooSpy()).toBe("foo");
  expect(barSpy()).toBe("bar");
  expect(bazSpy()).toBe("baz");
});

Использование mock.restore() может уменьшить количество кода в ваших тестах, добавив его в блоки afterEach в каждом тестовом файле или даже в вашем коде предварительной загрузки тестов.

Совместимость с Vitest

Для дополнительной совместимости с тестами, написанными для Vitest, Bun предоставляет глобальный объект vi как псевдоним для частей API мокирования Jest:

ts
import { test, expect } from "bun:test";

// Использование псевдонима 'vi', подобного Vitest
test("совместимость с vitest", () => {
  const mockFn = vi.fn(() => 42);

  mockFn();
  expect(mockFn).toHaveBeenCalled();

  // Следующие функции доступны на объекте vi:
  // vi.fn
  // vi.spyOn
  // vi.mock
  // vi.restoreAllMocks
  // vi.clearAllMocks
});

Это упрощает перенос тестов из Vitest в Bun без необходимости переписывать все ваши моки.

Детали реализации

Понимание того, как работает mock.module(), помогает использовать его более эффективно:

Взаимодействие с кэшем

Моки модулей взаимодействуют с кэшами модулей как ESM, так и CommonJS.

Ленивое вычисление

Обратный вызов фабрики моков вычисляется только тогда, когда модуль фактически импортируется или требуется.

Разрешение путей

Bun автоматически разрешает спецификатор модуля так, как будто вы делаете импорт, поддерживая:

  • Относительные пути ('./module')
  • Абсолютные пути ('/path/to/module')
  • Имена пакетов ('lodash')

Эффекты времени импорта

  • При мокировании до первого импорта: Никакие побочные эффекты оригинального модуля не происходят
  • При мокировании после импорта: Побочные эффекты оригинального модуля уже произошли

По этой причине использование --preload рекомендуется для моков, которые должны предотвратить побочные эффекты.

Живые привязки

Замоканые ESM-модули поддерживают живые привязки, поэтому изменение мока обновит все существующие импорты.

Продвинутые паттерны

Фабричные функции

ts
import { mock } from "bun:test";

function createMockUser(overrides = {}) {
  return {
    id: "mock-id",
    name: "Mock User",
    email: "mock@example.com",
    ...overrides,
  };
}

const mockUserService = {
  getUser: mock(async (id: string) => createMockUser({ id })),
  createUser: mock(async (data: any) => createMockUser(data)),
  updateUser: mock(async (id: string, data: any) => createMockUser({ id, ...data })),
};

Условное мокирование

ts
import { test, expect, mock } from "bun:test";

const shouldUseMockApi = process.env.NODE_ENV === "test";

if (shouldUseMockApi) {
  mock.module("./api", () => ({
    fetchData: mock(async () => ({ data: "mocked" })),
  }));
}

test("условное использование API", async () => {
  const { fetchData } = await import("./api");
  const result = await fetchData();

  if (shouldUseMockApi) {
    expect(result.data).toBe("mocked");
  }
});

Паттерны очистки моков

ts
import { afterEach, beforeEach } from "bun:test";

beforeEach(() => {
  // Настройка общих моков
  mock.module("./logger", () => ({
    log: mock(() => {}),
    error: mock(() => {}),
    warn: mock(() => {}),
  }));
});

afterEach(() => {
  // Очистка всех моков
  mock.restore();
  mock.clearAllMocks();
});

Лучшие практики

Держите моки простыми

ts
// Хорошо: Простой сфокусированный мок
const mockUserApi = {
  getUser: mock(async id => ({ id, name: "Test User" })),
};

// Избегайте: Слишком сложное поведение мока
const complexMock = mock(input => {
  if (input.type === "A") {
    return processTypeA(input);
  } else if (input.type === "B") {
    return processTypeB(input);
  }
  // ... много сложной логики
});

Используйте типобезопасные моки

ts
interface UserService {
  getUser(id: string): Promise<User>;
  createUser(data: CreateUserData): Promise<User>;
}

const mockUserService: UserService = {
  getUser: mock(async (id: string) => ({ id, name: "Test User" })),
  createUser: mock(async data => ({ id: "new-id", ...data })),
};

Тестируйте поведение моков

ts
test("сервис правильно вызывает API", async () => {
  const mockApi = { fetchUser: mock(async () => ({ id: "1" })) };

  const service = new UserService(mockApi);
  await service.getUser("123");

  // Проверить, что мок был вызван правильно
  expect(mockApi.fetchUser).toHaveBeenCalledWith("123");
  expect(mockApi.fetchUser).toHaveBeenCalledTimes(1);
});

Примечания

Автоматическое мокирование

Каталог __mocks__ и автоматическое мокирование пока не поддерживаются. Если это мешает вам переключиться на Bun, пожалуйста, создайте issue.

ESM против CommonJS

Моки модулей имеют различные реализации для модулей ESM и CommonJS. Для ES Modules Bun добавил патчи в JavaScriptCore, которые позволяют Bun переопределять значения экспорта во время выполнения и обновлять живые привязки рекурсивно.

Bun от www.bunjs.com.cn