Jest и Vitest это тестовые раннеры для JavaScript и TypeScript. Автоматизатор встречает их, когда пишет unit- и интеграционные тесты вместе с фронтендом или Node-бэкендом, а ещё когда API-тесты живут на TypeScript. API у них почти одинаковый: Vitest сознательно совместим с Jest.
Чем они отличаются
- Jest: давний стандарт, своя система трансформации (Babel или ts-jest), огромная экосистема.
- Vitest: работает поверх Vite, переиспользует его конфиг и плагины, нативно понимает ESM и TypeScript, быстро стартует. В новых проектах на Vite его выбирают по умолчанию.
Структура теста
import { describe, it, expect, beforeEach } from 'vitest';
describe('Cart', () => {
let cart: Cart;
beforeEach(() => { cart = new Cart(); });
it('считает сумму', () => {
cart.add({ sku: 'A', price: 100, qty: 2 });
expect(cart.total()).toBe(200);
});
it.each([
[0, 0], [999, 0], [1000, 100],
])('скидка для суммы %i равна %i', (sum, discount) => {
expect(discountFor(sum)).toBe(discount);
});
});
Хуки: beforeAll, beforeEach, afterEach, afterAll. Фокус и пропуск: it.only, it.skip, it.todo.
Матчеры, которые путают
toBeсравнивает черезObject.is: для объектов это проверка ссылки.toEqualрекурсивно сравнивает значения и игнорирует свойства со значениемundefined.toStrictEqualстроже: учитываетundefined-свойства, дырки в массивах и класс объекта.toMatchObjectпроверяет подмножество: в объекте должны быть указанные поля, лишние не мешают.toThrow,resolves,rejectsдля ошибок и промисов.
Асинхронность
Тест должен вернуть промис или быть async, иначе он закончится раньше проверки:
it('загружает профиль', async () => {
await expect(api.getProfile(1)).resolves.toMatchObject({ id: 1 });
});
Забытый await перед expect(...).resolves частая причина «зелёных» тестов, которые ничего не проверили.
Моки
const send = vi.fn().mockResolvedValue({ ok: true }); // в Jest: jest.fn()
vi.mock('./mailer', () => ({ send })); // подмена модуля, поднимается в начало файла
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
expect(send).toHaveBeenCalledTimes(1);
expect(send).toHaveBeenCalledWith(expect.objectContaining({ to: 'a@b.ru' }));
vi.mock и jest.mock поднимаются (hoisting) над импортами, поэтому внутри фабрики нельзя пользоваться переменными, объявленными ниже обычным способом. Между тестами моки сбрасывают: mockClear (история вызовов), mockReset (плюс реализация), mockRestore (вернуть оригинал для spyOn), или настройками clearMocks, restoreMocks.
Фейковые таймеры
vi.useFakeTimers();
const cb = vi.fn();
debounce(cb, 300)();
vi.advanceTimersByTime(299); expect(cb).not.toHaveBeenCalled();
vi.advanceTimersByTime(1); expect(cb).toHaveBeenCalledOnce();
vi.useRealTimers();
Тест на 300 мс задержку идёт мгновенно и без флаки.
Снапшоты
toMatchSnapshot() сохраняет сериализованный результат в файл и потом сравнивает. Удобно для больших структур, но легко превращается в «обновил снапшот, не глядя» (-u). Снапшот должен быть маленьким и проверяться на ревью.
Окружение и покрытие
- Окружение:
nodeпо умолчанию; для DOM нуженjsdomилиhappy-dom(testEnvironmentв Jest,environmentв Vitest). - Покрытие:
--coverage. В Vitest провайдер по умолчанию v8, можно istanbul. Пороги (coverageThresholdв Jest,coverage.thresholdsв Vitest) роняют прогон, если покрытие ниже.
Частые ошибки
toBeдля объектов и массивов.- Нет
awaitу асинхронной проверки. - Моки не сбрасываются между тестами, и тесты влияют друг на друга.
- Огромные снапшоты, которые обновляют не читая.
- Настоящие таймеры и
setTimeoutв тестах вместо фейковых.
Что спросят на собесе
- Чем
toBeотличается отtoEqualиtoStrictEqual? - Как замокать модуль и проверить, с какими аргументами вызвали функцию?
- Как тестировать код с таймерами?
- Чем Vitest отличается от Jest?
- Когда снапшоты полезны, а когда вредны?