Определяйте тесты с помощью API, подобного Jest, импортированного из встроенного модуля bun:test. В долгосрочной перспективе Bun стремится к полной совместимости с Jest; в настоящее время поддерживается ограниченный набор матчеров expect.
Базовое использование
Для определения простого теста:
import { expect, test } from "bun:test";
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});Группировка тестов
Тесты могут быть сгруппированы в наборы с помощью describe.
import { expect, test, describe } from "bun:test";
describe("арифметика", () => {
test("2 + 2", () => {
expect(2 + 2).toBe(4);
});
test("2 * 2", () => {
expect(2 * 2).toBe(4);
});
});Асинхронные тесты
Тесты могут быть асинхронными.
import { expect, test } from "bun:test";
test("2 * 2", async () => {
const result = await Promise.resolve(2 * 2);
expect(result).toEqual(4);
});В качестве альтернативы используйте обратный вызов done для сигнализации завершения. Если вы включаете обратный вызов done в качестве параметра в определении теста, вы должны вызвать его, иначе тест зависнет.
import { expect, test } from "bun:test";
test("2 * 2", done => {
Promise.resolve(2 * 2).then(result => {
expect(result).toEqual(4);
done();
});
});Таймауты
При необходимости укажите таймаут для каждого теста в миллисекундах, передав число в качестве третьего аргумента в test.
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 для автоматической повторной попытки теста, если он не удался. Тест проходит, если он успешен в пределах указанного количества попыток. Это полезно для ненадёжных тестов, которые могут периодически завершаться неудачей.
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 повторений).
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. Эти тесты не будут запущены.
import { expect, test } from "bun:test";
test.skip("wat", () => {
// TODO: исправить это
expect(0.1 + 0.2).toEqual(0.3);
});test.todo
Отметьте тест как todo с test.todo. Эти тесты не будут запущены.
import { expect, test } from "bun:test";
test.todo("исправить это", () => {
myTestFunction();
});Для запуска todo тестов и поиска тех, которые проходят, используйте bun test --todo.
bun test --todomy.test.ts:
✗ нереализованная функция
^ этот тест помечен как todo, но проходит. Удалите `.todo` или проверьте, что тест корректен.
0 pass
1 fail
1 expect() callsС этим флагом неудачные todo тесты не вызовут ошибку, но todo тесты, которые проходят, будут помечены как неудачные, чтобы вы могли удалить пометку todo или исправить тест.
test.only
Для запуска конкретного теста или набора тестов используйте test.only() или describe.only().
import { test, describe } from "bun:test";
test("тест #1", () => {
// не запускается
});
test.only("тест #2", () => {
// запускается
});
describe.only("only", () => {
test("тест #3", () => {
// запускается
});
});Следующая команда запустит только тесты #2 и #3.
bun test --onlyСледующая команда запустит только тесты #1, #2 и #3.
bun testtest.if
Для условного запуска теста используйте test.if(). Тест будет запущен, если условие истинно. Это особенно полезно для тестов, которые должны запускаться только для определённых архитектур или операционных систем.
test.if(Math.random() > 0.5)("запускается половину времени", () => {
// ...
});
const macOS = process.platform === "darwin";
test.if(macOS)("запускается на macOS", () => {
// запускается, если macOS
});test.skipIf
Для пропуска теста на основе некоторого условия используйте test.skipIf() или describe.skipIf().
const macOS = process.platform === "darwin";
test.skipIf(macOS)("запускается на не-macOS", () => {
// запускается, если *не* macOS
});test.todoIf
Если вы хотите отметить тест как TODO, используйте test.todoIf() или describe.todoIf(). Тщательный выбор skipIf или todoIf может показать разницу между, например, намерением "недействителен для этой цели" и "запланирован, но ещё не реализован".
const macOS = process.platform === "darwin";
// TODO: мы реализовали это только для Linux пока что.
test.todoIf(macOS)("запускается на posix", () => {
// запускается, если *не* macOS
});test.failing
Используйте test.failing(), когда вы знаете, что тест в настоящее время не удался, но вы хотите отслеживать его и получать уведомление, когда он начнёт проходить. Это инвертирует результат теста:
- Неудачный тест, помеченный с
.failing(), пройдёт - Проходящий тест, помеченный с
.failing(), завершится неудачей (с сообщением, указывающим, что он теперь проходит и должен быть исправлен)
// Это пройдёт, потому что тест не удался, как ожидалось
test.failing("математика сломана", () => {
expect(0.1 + 0.2).toBe(0.3); // не удаётся из-за точности чисел с плавающей точкой
});
// Это не удастся с сообщением, что тест теперь проходит
test.failing("исправленная ошибка", () => {
expect(1 + 1).toBe(2); // проходит, но мы ожидали, что он не удастся
});Это полезно для отслеживания известных ошибок, которые вы планируете исправить позже, или для реализации тест-драйвенной разработки.
Условные тесты для блоков Describe
Условные модификаторы .if(), .skipIf() и .todoIf() также могут быть применены к блокам describe, влияя на все тесты внутри набора:
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. Это создаёт параметризованный тест, который запускается один раз для каждого предоставленного тестового случая.
const cases = [
[1, 2, 3],
[3, 4, 7],
];
test.each(cases)("%p + %p должно быть %p", (a, b, expected) => {
expect(a + b).toBe(expected);
});Вы также можете использовать describe.each для создания параметризованного набора, который запускается один раз для каждого тестового случая:
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]), каждый элемент передаётся как отдельный аргумент - Если строка не является массивом (как объект), она передаётся как один аргумент
// Элементы массива передаются как отдельные аргументы
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);
});Спецификаторы формата
Есть несколько доступных опций для форматирования заголовка теста:
| Спецификатор | Описание |
|---|---|
%p | pretty-format |
%s | String |
%d | Number |
%i | Integer |
%f | Floating point |
%j | JSON |
%o | Object |
%# | Индекс тестового случая |
%% | Одиночный знак процента (%) |
Примеры
// Базовые спецификаторы
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() для проверки, что хотя бы одно утверждение было вызвано во время теста:
test("асинхронная работа вызывает утверждения", async () => {
expect.hasAssertions(); // Не удастся, если не вызвано ни одного утверждения
const data = await fetchData();
expect(data).toBeDefined();
});Это особенно полезно для асинхронных тестов, чтобы убедиться, что ваши утверждения действительно выполняются.
expect.assertions(count)
Используйте expect.assertions(count) для проверки, что конкретное количество утверждений было вызвано во время теста:
test("ровно два утверждения", () => {
expect.assertions(2); // Не удастся, если вызвано не ровно 2 утверждения
expect(1 + 1).toBe(2);
expect("hello").toContain("ell");
});Это помогает убедиться, что все ваши утверждения выполняются, особенно в сложном асинхронном коде с несколькими путями кода.
Тестирование типов
Bun включает expectTypeOf для тестирования типов TypeScript, совместимый с Vitest.
expectTypeOf
Функция expectTypeOf предоставляет утверждения на уровне типов, которые проверяются проверкой типов TypeScript. Для тестирования ваших типов:
- Напишите ваши утверждения типов, используя
expectTypeOf - Запустите
bunx tsc --noEmitдля проверки, что ваши типы корректны
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() |
Лучшие практики
Используйте описательные имена тестов
// Хорошо
test("должен вычислять общую цену включая налог для нескольких товаров", () => {
// реализация теста
});
// Избегайте
test("вычисление цены", () => {
// реализация теста
});Группируйте связанные тесты
describe("Аутентификация пользователя", () => {
describe("с корректными учётными данными", () => {
test("должен возвращать данные пользователя", () => {
// реализация теста
});
test("должен устанавливать токен аутентификации", () => {
// реализация теста
});
});
describe("с некорректными учётными данными", () => {
test("должен выбрасывать ошибку аутентификации", () => {
// реализация теста
});
});
});Используйте соответствующие матчеры
// Хорошо: Использование конкретных матчеров
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);Тестируйте условия ошибок
test("должен выбрасывать ошибку для некорректного ввода", () => {
expect(() => {
validateEmail("not-an-email");
}).toThrow("Некорректный формат email");
});
test("должен обрабатывать асинхронные ошибки", async () => {
await expect(async () => {
await fetchUser("invalid-id");
}).rejects.toThrow("Пользователь не найден");
});Используйте настройку и очистку
import { beforeEach, afterEach, test } from "bun:test";
let testUser;
beforeEach(() => {
testUser = createTestUser();
});
afterEach(() => {
cleanupTestUser(testUser);
});
test("должен обновлять профиль пользователя", () => {
// Использовать testUser в тесте
});