Тестирование снимков сохраняет значение и сравнивает его с будущими запусками тестов. Это особенно полезно для UI-компонентов, сложных объектов или любого вывода, который должен оставаться согласованным.
Базовые снимки
Тесты снимков пишутся с использованием матчера .toMatchSnapshot():
import { test, expect } from "bun:test";
test("снимок", () => {
expect("foo").toMatchSnapshot();
});Первый раз, когда этот тест запускается, аргумент expect будет сериализован и записан в специальный файл снимков в директории __snapshots__ рядом с тестовым файлом.
Файлы снимков
После запуска теста выше Bun создаст:
ваш-проект/
├── snap.test.ts
└── __snapshots__/
└── snap.test.ts.snapФайл снимков содержит:
// Bun Snapshot v1, https://bun.com/docs/test/snapshots
exports[`snap 1`] = `"foo"`;При будущих запусках аргумент сравнивается со снимком на диске.
Обновление снимков
Снимки могут быть перегенерированы следующей командой:
bun test --update-snapshotsЭто полезно, когда:
- Вы намеренно изменили вывод
- Вы добавляете новые тесты снимков
- Ожидаемый вывод изменился легитимно
Встроенные снимки
Для меньших значений вы можете использовать встроенные снимки с .toMatchInlineSnapshot(). Эти снимки хранятся прямо в вашем тестовом файле:
import { test, expect } from "bun:test";
test("встроенный снимок", () => {
// Первый запуск: снимок будет вставлен автоматически
expect({ hello: "world" }).toMatchInlineSnapshot();
});После первого запуска Bun автоматически обновляет ваш тестовый файл:
import { test, expect } from "bun:test";
test("встроенный снимок", () => {
expect({ hello: "world" }).toMatchInlineSnapshot(`
{
"hello": "world",
}
`);
});Использование встроенных снимков
- Напишите ваш тест с
.toMatchInlineSnapshot() - Запустите тест один раз
- Bun автоматически обновит ваш тестовый файл снимком
- При последующих запусках значение будет сравниваться со встроенным снимком
Встроенные снимки особенно полезны для небольших простых значений, где полезно видеть ожидаемый вывод прямо в тестовом файле.
Снимки ошибок
Вы также можете сохранять снимки сообщений об ошибках, используя .toThrowErrorMatchingSnapshot() и .toThrowErrorMatchingInlineSnapshot():
import { test, expect } from "bun:test";
test("снимок ошибки", () => {
expect(() => {
throw new Error("Что-то пошло не так");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw new Error("Другая ошибка");
}).toThrowErrorMatchingInlineSnapshot();
});После запуска встроенная версия становится:
test("снимок ошибки", () => {
expect(() => {
throw new Error("Что-то пошло не так");
}).toThrowErrorMatchingSnapshot();
expect(() => {
throw new Error("Другая ошибка");
}).toThrowErrorMatchingInlineSnapshot(`"Другая ошибка"`);
});Продвинутое использование снимков
Сложные объекты
Снимки хорошо работают со сложными вложенными объектами:
import { test, expect } from "bun:test";
test("снимок сложного объекта", () => {
const user = {
id: 1,
name: "John Doe",
email: "john@example.com",
profile: {
age: 30,
preferences: {
theme: "dark",
notifications: true,
},
},
tags: ["developer", "javascript", "bun"],
};
expect(user).toMatchSnapshot();
});Снимки массивов
Массивы также хорошо подходят для тестирования снимков:
import { test, expect } from "bun:test";
test("снимок массива", () => {
const numbers = [1, 2, 3, 4, 5].map(n => n * 2);
expect(numbers).toMatchSnapshot();
});Снимки вывода функций
Сохраняйте снимки вывода функций:
import { test, expect } from "bun:test";
function generateReport(data: any[]) {
return {
total: data.length,
summary: data.map(item => ({ id: item.id, name: item.name })),
timestamp: "2024-01-01", // Фиксировано для тестирования
};
}
test("генерация отчёта", () => {
const data = [
{ id: 1, name: "Alice", age: 30 },
{ id: 2, name: "Bob", age: 25 },
];
expect(generateReport(data)).toMatchSnapshot();
});Снимки React-компонентов
Снимки особенно полезны для React-компонентов:
import { test, expect } from "bun:test";
import { render } from "@testing-library/react";
function Button({ children, variant = "primary" }) {
return <button className={`btn btn-${variant}`}>{children}</button>;
}
test("снимки компонента Button", () => {
const { container: primary } = render(<Button>Нажми меня</Button>);
const { container: secondary } = render(<Button variant="secondary">Отмена</Button>);
expect(primary.innerHTML).toMatchSnapshot();
expect(secondary.innerHTML).toMatchSnapshot();
});Матчеры свойств
Для значений, которые меняются между запусками тестов (например, временные метки или ID), используйте матчеры свойств:
import { test, expect } from "bun:test";
test("снимок с динамическими значениями", () => {
const user = {
id: Math.random(), // Это меняется каждый запуск
name: "John",
createdAt: new Date().toISOString(), // Это тоже меняется
};
expect(user).toMatchSnapshot({
id: expect.any(Number),
createdAt: expect.any(String),
});
});Снимок сохранит:
exports[`snapshot with dynamic values 1`] = `
{
"createdAt": Any<String>,
"id": Any<Number>,
"name": "John",
}
`;Пользовательские сериализаторы
Вы можете настроить, как объекты сериализуются в снимках:
import { test, expect } from "bun:test";
// Пользовательский сериализатор для объектов Date
expect.addSnapshotSerializer({
test: val => val instanceof Date,
serialize: val => `"${val.toISOString()}"`,
});
test("пользовательский сериализатор", () => {
const event = {
name: "Встреча",
date: new Date("2024-01-01T10:00:00Z"),
};
expect(event).toMatchSnapshot();
});Лучшие практики
Держите снимки небольшими
// Хорошо: Сфокусированные снимки
test("форматирование имени пользователя", () => {
const formatted = formatUserName("john", "doe");
expect(formatted).toMatchInlineSnapshot(`"John Doe"`);
});
// Избегайте: Огромные снимки, которые трудно рецензировать
test("отрисовка всей страницы", () => {
const page = renderEntirePage();
expect(page).toMatchSnapshot(); // Это может быть тысячи строк
});Используйте описательные имена тестов
// Хорошо: Ясно, что представляет снимок
test("форматирует валюту с символом USD", () => {
expect(formatCurrency(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});
// Избегайте: Непонятно, что тестируется
test("тест форматирования", () => {
expect(format(99.99)).toMatchInlineSnapshot(`"$99.99"`);
});Группируйте связанные снимки
import { describe, test, expect } from "bun:test";
describe("компонент Button", () => {
test("вариант primary", () => {
expect(render(<Button variant="primary">Нажми</Button>))
.toMatchSnapshot();
});
test("вариант secondary", () => {
expect(render(<Button variant="secondary">Отмена</Button>))
.toMatchSnapshot();
});
test("состояние disabled", () => {
expect(render(<Button disabled>Отключено</Button>))
.toMatchSnapshot();
});
});Обработка динамических данных
// Хорошо: Нормализация динамических данных
test("формат ответа API", () => {
const response = {
data: { id: 1, name: "Test" },
timestamp: Date.now(),
requestId: generateId(),
};
expect({
...response,
timestamp: "TIMESTAMP",
requestId: "REQUEST_ID",
}).toMatchSnapshot();
});
// Или используйте матчеры свойств
test("ответ API с матчерами", () => {
const response = getApiResponse();
expect(response).toMatchSnapshot({
timestamp: expect.any(Number),
requestId: expect.any(String),
});
});Управление снимками
Рецензирование изменений снимков
Когда снимки меняются, внимательно рецензируйте их:
# Посмотреть, что изменилось
git diff __snapshots__/
# Обновить, если изменения намеренны
bun test --update-snapshots
# Закоммитить обновлённые снимки
git add __snapshots__/
git commit -m "Обновить снимки после изменений UI"Очистка неиспользуемых снимков
Bun предупредит о неиспользуемых снимках:
Warning: 1 unused snapshot found:
my-test.test.ts.snap: "old test that no longer exists 1"Удалите неиспользуемые снимки, удалив их из файлов снимков или запустив тесты с флагами очистки, если доступно.
Организация больших файлов снимков
Для больших проектов рассмотрите организацию тестов для поддержания управляемости файлов снимков:
tests/
├── components/
│ ├── Button.test.tsx
│ └── __snapshots__/
│ └── Button.test.tsx.snap
├── utils/
│ ├── formatters.test.ts
│ └── __snapshots__/
│ └── formatters.test.ts.snapУстранение неполадок
Неудачи снимков
Когда снимки не совпадают, вы увидите diff:
- Ожидается
+ Получено
Object {
- "name": "John",
+ "name": "Jane",
}Распространённые причины:
- Намеренные изменения (обновите с
--update-snapshots) - Ненамеренные изменения (исправьте код)
- Динамические данные (используйте матчеры свойств)
- Различия окружения (нормализуйте данные)
Различия платформ
Будьте внимательны к различиям платформ:
// Пути могут отличаться между Windows/Unix
test("операции с файлами", () => {
const result = processFile("./test.txt");
expect({
...result,
path: result.path.replace(/\\/g, "/"), // Нормализация путей
}).toMatchSnapshot();
});