Skip to content

Определяйте тесты с помощью API, подобного Jest, импортированного из встроенного модуля bun:test. В долгосрочной перспективе Bun стремится к полной совместимости с Jest; в настоящее время поддерживается ограниченный набор матчеров expect.

Базовое использование ​

Для определения простого теста:

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

test("2 + 2", () => {
  expect(2 + 2).toBe(4);
});

Группировка тестов ​

Тесты могут быть сгруппированы в наборы с помощью describe.

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

describe("арифметика", () => {
  test("2 + 2", () => {
    expect(2 + 2).toBe(4);
  });

  test("2 * 2", () => {
    expect(2 * 2).toBe(4);
  });
});

Асинхронные тесты ​

Тесты могут быть асинхронными.

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

test("2 * 2", async () => {
  const result = await Promise.resolve(2 * 2);
  expect(result).toEqual(4);
});

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

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

test("2 * 2", done => {
  Promise.resolve(2 * 2).then(result => {
    expect(result).toEqual(4);
    done();
  });
});

Таймауты ​

При необходимости укажите таймаут для каждого теста в миллисекундах, передав число в качестве третьего аргумента в test.

math.test.ts
ts
import { test } from "bun:test";

test("wat", async () => {
  const data = await slowOperation();
  expect(data).toBe(42);
}, 500); // тест должен выполниться за <500мс

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

Таймаут по умолчанию для каждого теста составляет 5000 мс (5 секунд), если не переопределён этой опцией таймаута или jest.setDefaultTimeout().

Повторные попытки и повторения ​

test.retry ​

Используйте опцию retry для автоматической повторной попытки теста, если он не удался. Тест проходит, если он успешен в пределах указанного количества попыток. Это полезно для ненадёжных тестов, которые могут периодически завершаться неудачей.

example.test.ts
ts
import { test } from "bun:test";

test(
  "ненадёжный сетевой запрос",
  async () => {
    const response = await fetch("https://example.com/api");
    expect(response.ok).toBe(true);
  },
  { retry: 3 }, // Повторить до 3 раз, если тест не удался
);

test.repeats ​

Используйте опцию repeats для запуска теста несколько раз независимо от статуса прохождения/неудачи. Тест завершается неудачей, если любая итерация не удалась. Это полезно для обнаружения ненадёжных тестов или стресс-тестирования. Обратите внимание, что repeats: N запускает тест N+1 раз всего (1 начальный запуск + N повторений).

example.test.ts
ts
import { test } from "bun:test";

test(
  "убедиться, что тест стабилен",
  () => {
    expect(Math.random()).toBeLessThan(1);
  },
  { repeats: 20 }, // Запускается 21 раз всего (1 начальный + 20 повторений)
);

NOTE

Вы не можете использовать одновременно `retry` и `repeats` в одном тесте.

🧟 Убийца зомби-процессов ​

Когда тест истекает и процессы, порождённые в тесте через Bun.spawn, Bun.spawnSync или node:child_process, не убиты, они будут автоматически убиты, и сообщение будет записано в консоль. Это предотвращает задержку зомби-процессов в фоне после истёкших тестов.

Модификаторы тестов ​

test.skip ​

Пропускайте отдельные тесты с test.skip. Эти тесты не будут запущены.

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

test.skip("wat", () => {
  // TODO: исправить это
  expect(0.1 + 0.2).toEqual(0.3);
});

test.todo ​

Отметьте тест как todo с test.todo. Эти тесты не будут запущены.

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

test.todo("исправить это", () => {
  myTestFunction();
});

Для запуска todo тестов и поиска тех, которые проходят, используйте bun test --todo.

bash
bun test --todo
my.test.ts:
✗ нереализованная функция
  ^ этот тест помечен как todo, но проходит. Удалите `.todo` или проверьте, что тест корректен.

 0 pass
 1 fail
 1 expect() calls

С этим флагом неудачные todo тесты не вызовут ошибку, но todo тесты, которые проходят, будут помечены как неудачные, чтобы вы могли удалить пометку todo или исправить тест.

test.only ​

Для запуска конкретного теста или набора тестов используйте test.only() или describe.only().

example.test.ts
ts
import { test, describe } from "bun:test";

test("тест #1", () => {
  // не запускается
});

test.only("тест #2", () => {
  // запускается
});

describe.only("only", () => {
  test("тест #3", () => {
    // запускается
  });
});

Следующая команда запустит только тесты #2 и #3.

bash
bun test --only

Следующая команда запустит только тесты #1, #2 и #3.

bash
bun test

test.if ​

Для условного запуска теста используйте test.if(). Тест будет запущен, если условие истинно. Это особенно полезно для тестов, которые должны запускаться только для определённых архитектур или операционных систем.

example.test.ts
ts
test.if(Math.random() > 0.5)("запускается половину времени", () => {
  // ...
});

const macOS = process.platform === "darwin";
test.if(macOS)("запускается на macOS", () => {
  // запускается, если macOS
});

test.skipIf ​

Для пропуска теста на основе некоторого условия используйте test.skipIf() или describe.skipIf().

example.test.ts
ts
const macOS = process.platform === "darwin";

test.skipIf(macOS)("запускается на не-macOS", () => {
  // запускается, если *не* macOS
});

test.todoIf ​

Если вы хотите отметить тест как TODO, используйте test.todoIf() или describe.todoIf(). Тщательный выбор skipIf или todoIf может показать разницу между, например, намерением "недействителен для этой цели" и "запланирован, но ещё не реализован".

example.test.ts
ts
const macOS = process.platform === "darwin";

// TODO: мы реализовали это только для Linux пока что.
test.todoIf(macOS)("запускается на posix", () => {
  // запускается, если *не* macOS
});

test.failing ​

Используйте test.failing(), когда вы знаете, что тест в настоящее время не удался, но вы хотите отслеживать его и получать уведомление, когда он начнёт проходить. Это инвертирует результат теста:

  • Неудачный тест, помеченный с .failing(), пройдёт
  • Проходящий тест, помеченный с .failing(), завершится неудачей (с сообщением, указывающим, что он теперь проходит и должен быть исправлен)
ts
// Это пройдёт, потому что тест не удался, как ожидалось
test.failing("математика сломана", () => {
  expect(0.1 + 0.2).toBe(0.3); // не удаётся из-за точности чисел с плавающей точкой
});

// Это не удастся с сообщением, что тест теперь проходит
test.failing("исправленная ошибка", () => {
  expect(1 + 1).toBe(2); // проходит, но мы ожидали, что он не удастся
});

Это полезно для отслеживания известных ошибок, которые вы планируете исправить позже, или для реализации тест-драйвенной разработки.

Условные тесты для блоков Describe ​

Условные модификаторы .if(), .skipIf() и .todoIf() также могут быть применены к блокам describe, влияя на все тесты внутри набора:

example.test.ts
ts
const isMacOS = process.platform === "darwin";

// Запускается только весь набор на macOS
describe.if(isMacOS)("специфичные для macOS функции", () => {
  test("функция A", () => {
    // запускается только на macOS
  });

  test("функция B", () => {
    // запускается только на macOS
  });
});

// Пропускается весь набор на Windows
describe.skipIf(process.platform === "win32")("Unix функции", () => {
  test("функция C", () => {
    // пропускается на Windows
  });
});

// Отмечается весь набор как TODO на Linux
describe.todoIf(process.platform === "linux")("Предстоящая поддержка Linux", () => {
  test("функция D", () => {
    // помечено как TODO на Linux
  });
});

Параметризованные тесты ​

test.each и describe.each ​

Для запуска одного и того же теста с несколькими наборами данных используйте test.each. Это создаёт параметризованный тест, который запускается один раз для каждого предоставленного тестового случая.

math.test.ts
ts
const cases = [
  [1, 2, 3],
  [3, 4, 7],
];

test.each(cases)("%p + %p должно быть %p", (a, b, expected) => {
  expect(a + b).toBe(expected);
});

Вы также можете использовать describe.each для создания параметризованного набора, который запускается один раз для каждого тестового случая:

sum.test.ts
ts
describe.each([
  [1, 2, 3],
  [3, 4, 7],
])("add(%i, %i)", (a, b, expected) => {
  test(`возвращает ${expected}`, () => {
    expect(a + b).toBe(expected);
  });

  test(`сумма больше каждого значения`, () => {
    expect(a + b).toBeGreaterThan(a);
    expect(a + b).toBeGreaterThan(b);
  });
});

Передача аргументов ​

То, как аргументы передаются в вашу тестовую функцию, зависит от структуры ваших тестовых случаев:

  • Если строка таблицы является массивом (как [1, 2, 3]), каждый элемент передаётся как отдельный аргумент
  • Если строка не является массивом (как объект), она передаётся как один аргумент
example.test.ts
ts
// Элементы массива передаются как отдельные аргументы
test.each([
  [1, 2, 3],
  [4, 5, 9],
])("add(%i, %i) = %i", (a, b, expected) => {
  expect(a + b).toBe(expected);
});

// Элементы объекта передаются как один аргумент
test.each([
  { a: 1, b: 2, expected: 3 },
  { a: 4, b: 5, expected: 9 },
])("add($a, $b) = $expected", data => {
  expect(data.a + data.b).toBe(data.expected);
});

Спецификаторы формата ​

Есть несколько доступных опций для форматирования заголовка теста:

СпецификаторОписание
%ppretty-format
%sString
%dNumber
%iInteger
%fFloating point
%jJSON
%oObject
%#Индекс тестового случая
%%Одиночный знак процента (%)

Примеры ​

example.test.ts
ts
// Базовые спецификаторы
test.each([
  ["hello", 123],
  ["world", 456],
])("string: %s, number: %i", (str, num) => {
  // "string: hello, number: 123"
  // "string: world, number: 456"
});

// %p для вывода pretty-format
test.each([
  [{ name: "Alice" }, { a: 1, b: 2 }],
  [{ name: "Bob" }, { x: 5, y: 10 }],
])("user %p with data %p", (user, data) => {
  // "user { name: 'Alice' } with data { a: 1, b: 2 }"
  // "user { name: 'Bob' } with data { x: 5, y: 10 }"
});

// %# для индекса
test.each(["apple", "banana"])("fruit #%# is %s", fruit => {
  // "fruit #0 is apple"
  // "fruit #1 is banana"
});

Подсчёт утверждений ​

Bun поддерживает проверку того, что конкретное количество утверждений было вызвано во время теста:

expect.hasAssertions() ​

Используйте expect.hasAssertions() для проверки, что хотя бы одно утверждение было вызвано во время теста:

example.test.ts
ts
test("асинхронная работа вызывает утверждения", async () => {
  expect.hasAssertions(); // Не удастся, если не вызвано ни одного утверждения

  const data = await fetchData();
  expect(data).toBeDefined();
});

Это особенно полезно для асинхронных тестов, чтобы убедиться, что ваши утверждения действительно выполняются.

expect.assertions(count) ​

Используйте expect.assertions(count) для проверки, что конкретное количество утверждений было вызвано во время теста:

example.test.ts
ts
test("ровно два утверждения", () => {
  expect.assertions(2); // Не удастся, если вызвано не ровно 2 утверждения

  expect(1 + 1).toBe(2);
  expect("hello").toContain("ell");
});

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

Тестирование типов ​

Bun включает expectTypeOf для тестирования типов TypeScript, совместимый с Vitest.

expectTypeOf ​

Функция expectTypeOf предоставляет утверждения на уровне типов, которые проверяются проверкой типов TypeScript. Для тестирования ваших типов:

  1. Напишите ваши утверждения типов, используя expectTypeOf
  2. Запустите bunx tsc --noEmit для проверки, что ваши типы корректны
example.test.ts
ts
import { expectTypeOf } from "bun:test";

// Базовые утверждения типов
expectTypeOf<string>().toEqualTypeOf<string>();
expectTypeOf(123).toBeNumber();
expectTypeOf("hello").toBeString();

// Сопоставление типов объектов
expectTypeOf({ a: 1, b: "hello" }).toMatchObjectType<{ a: number }>();

// Типы функций
function greet(name: string): string {
  return `Hello ${name}`;
}

expectTypeOf(greet).toBeFunction();
expectTypeOf(greet).parameters.toEqualTypeOf<[string]>();
expectTypeOf(greet).returns.toEqualTypeOf<string>();

// Типы массивов
expectTypeOf([1, 2, 3]).items.toBeNumber();

// Типы Promise
expectTypeOf(Promise.resolve(42)).resolves.toBeNumber();

Для полной документации по матчерам expectTypeOf см. Справочник API.

Матчеры ​

Bun реализует следующие матчеры. Полная совместимость с Jest находится в планах; отслеживайте прогресс здесь.

Базовые матчеры ​

СтатусМатчер
✅.not
✅.toBe()
✅.toEqual()
✅.toBeNull()
✅.toBeUndefined()
✅.toBeNaN()
✅.toBeDefined()
✅.toBeFalsy()
✅.toBeTruthy()
✅.toStrictEqual()

Матчеры строк и массивов ​

СтатусМатчер
✅.toContain()
✅.toHaveLength()
✅.toMatch()
✅.toContainEqual()
✅.stringContaining()
✅.stringMatching()
✅.arrayContaining()

Матчеры объектов ​

СтатусМатчер
✅.toHaveProperty()
✅.toMatchObject()
✅.toContainAllKeys()
✅.toContainValue()
✅.toContainValues()
✅.toContainAllValues()
✅.toContainAnyValues()
✅.objectContaining()

Матчеры чисел ​

СтатусМатчер
✅.toBeCloseTo()
✅.closeTo()
✅.toBeGreaterThan()
✅.toBeGreaterThanOrEqual()
✅.toBeLessThan()
✅.toBeLessThanOrEqual()

Матчеры функций и классов ​

СтатусМатчер
✅.toThrow()
✅.toBeInstanceOf()

Матчеры Promise ​

СтатусМатчер
✅.resolves()
✅.rejects()

Матчеры мок-функций ​

СтатусМатчер
✅.toHaveBeenCalled()
✅.toHaveBeenCalledTimes()
✅.toHaveBeenCalledWith()
✅.toHaveBeenLastCalledWith()
✅.toHaveBeenNthCalledWith()
✅.toHaveReturned()
✅.toHaveReturnedTimes()
✅.toHaveReturnedWith()
✅.toHaveLastReturnedWith()
✅.toHaveNthReturnedWith()

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

СтатусМатчер
✅.toMatchSnapshot()
✅.toMatchInlineSnapshot()
✅.toThrowErrorMatchingSnapshot()
✅.toThrowErrorMatchingInlineSnapshot()

Утилитарные матчеры ​

СтатусМатчер
✅.extend
✅.anything()
✅.any()
✅.assertions()
✅.hasAssertions()

Ещё не реализовано ​

СтатусМатчер
❌.addSnapshotSerializer()

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

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

example.test.ts
ts
// Хорошо
test("должен вычислять общую цену включая налог для нескольких товаров", () => {
  // реализация теста
});

// Избегайте
test("вычисление цены", () => {
  // реализация теста
});

Группируйте связанные тесты ​

auth.test.ts
ts
describe("Аутентификация пользователя", () => {
  describe("с корректными учётными данными", () => {
    test("должен возвращать данные пользователя", () => {
      // реализация теста
    });

    test("должен устанавливать токен аутентификации", () => {
      // реализация теста
    });
  });

  describe("с некорректными учётными данными", () => {
    test("должен выбрасывать ошибку аутентификации", () => {
      // реализация теста
    });
  });
});

Используйте соответствующие матчеры ​

auth.test.ts
ts
// Хорошо: Использование конкретных матчеров
expect(users).toHaveLength(3);
expect(user.email).toContain("@");
expect(response.status).toBeGreaterThanOrEqual(200);

// Избегайте: Использование toBe для всего
expect(users.length === 3).toBe(true);
expect(user.email.includes("@")).toBe(true);
expect(response.status >= 200).toBe(true);

Тестируйте условия ошибок ​

example.test.ts
ts
test("должен выбрасывать ошибку для некорректного ввода", () => {
  expect(() => {
    validateEmail("not-an-email");
  }).toThrow("Некорректный формат email");
});

test("должен обрабатывать асинхронные ошибки", async () => {
  await expect(async () => {
    await fetchUser("invalid-id");
  }).rejects.toThrow("Пользователь не найден");
});

Используйте настройку и очистку ​

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

let testUser;

beforeEach(() => {
  testUser = createTestUser();
});

afterEach(() => {
  cleanupTestUser(testUser);
});

test("должен обновлять профиль пользователя", () => {
  // Использовать testUser в тесте
});

Bun от www.bunjs.com.cn