Тесты живут в одном процессе, идут друг за другом и делят память. Отсюда два способа солгать. Первый - утёкшее состояние: один тест меняет общий объект, и следующий проходит или падает не по своей причине. Второй - незавершённая асинхронность: проверка идёт раньше, чем работа закончилась. Обе дисциплины, чистый мир и честное ожидание, служат детерминизму: результат зависит от кода, а не от порядка и гонки.
Чистый мир значит, что каждый тест стартует с заново собранного окружения. Возьмём CartService поверх InMemoryCartRepository: в beforeEach создаём свежий репозиторий и новый сервис перед каждым тестом. Тогда один тест не видит товары, добавленные другим, и любой тест можно запустить в одиночку с тем же результатом. Пересборка дешёвая, зато тесты независимы: падение указывает на код, а не на соседа по файлу.
Соблазн - собрать сервис один раз в beforeAll: кажется, что так быстрее. Но как только хотя бы один тест мутирует общий репозиторий, состояние протекает в следующие, и suite становится порядкозависимым: зелёный целиком, но падает, если запустить один тест или поменять порядок.
beforeAll оправдан для дорогого и неизменяемого ресурса: поднять контейнер с базой или большой фикстур-файл. Как только речь о mutable-состоянии, возвращайтесь к beforeEach. afterEach - для симметричной уборки: закрыть соединение, снять слушатель, вернуть глобальную настройку.
Вложенные describe удобны, пока задают контекст: корзина, пустая корзина, корзина с товаром. Опасность в другом: когда хуков много и они разбросаны по уровням вложенности, тест собирает окружение из неявных beforeEach где-то выше, и уже неясно, с каким состоянием имеешь дело.
Моки - тоже общее состояние, и их надо чистить между тестами. clearMocks очищает историю вызовов и результатов. resetMocks делает то же и вдобавок сбрасывает заданные реализации и возвращаемые значения. restoreMocks возвращает оригиналы для spy и подменённых свойств. В Jest 30 все три выключены по умолчанию, поэтому изоляцию моков включают явно.
| Настройка | Что делает |
|---|---|
| clearMocks |
| очищает историю calls/results |
| resetMocks | также сбрасывает реализации mock-функций |
|---|
| restoreMocks | возвращает оригиналы spy/replaced properties |
|---|
Асинхронность добавляет второй способ солгать, и самый частый ложный зелёный в Jest - забытый return или await на промисе с проверкой. Если тест запускает промис, но не возвращает и не ждёт его, Jest видит, что синхронная часть завершилась без ошибки, и помечает тест пройденным. А expect внутри .then выполнится позже - уже в чужом тесте или не выполнится вовсе. Лечится просто: тест обязан вернуть промис или быть async и await-нуть работу, тогда Jest дождётся настоящего результата.
Для промисов есть прямые матчеры: await expect(load()).resolves.toMatchObject(...) ждёт разрешения, а await expect(p).rejects.toThrow(...) проверяет отклонение с нужной ошибкой - обязательно с await, иначе снова плавающий промис. Когда проверка спрятана в условной ветке или в catch, подстрахуйтесь: expect.assertions(1) требует ровно одно утверждение, expect.hasAssertions() - хотя бы одно, и тест упадёт, если ожидаемый expect не выполнился. И забудьте про устаревший колбэк done: он глотает ошибки из then, а забытый вызов done превращает падение в таймаут.
Ещё ловушка - непойманное отклонение промиса: всплывает асинхронно и часто приписывается следующему тесту, так что красный горит не там, где ошибка. Ловите отклонения там, где их ждёте, а не оставляйте болтаться. Итог: без чистого мира и честного ожидания suite бывает зелёным, ничего не доказав, и рано или поздно замигает flaky.
describe('CartService', () => {
let repo: InMemoryCartRepository
let service: CartService
beforeEach(() => {
repo = new InMemoryCartRepository()
service = new CartService(repo)
})
test('добавляет товар', async () => {
await service.add('cart-1', product)
await expect(repo.find('cart-1')).resolves.toMatchObject({
items: [{ sku: product.sku, quantity: 1 }],
})
})
})test('loads', () => {
load().then(data => {
expect(data.ok).toBe(true)
})
})test('loads', async () => {
await expect(load()).resolves.toMatchObject({
ok: true,
})
})test('отклоняет неизвестного пользователя', async () => {
expect.assertions(1)
await expect(loadUser('missing')).rejects.toThrow('User not found')
})