Каждый тест должен начинаться с чистого мира, иначе результат начинает зависеть от порядка запуска, а это первый шаг к флаку. Для этого есть четыре хука: beforeEach и afterEach выполняются вокруг каждого теста, beforeAll и afterAll - один раз на весь describe. Выбор между ними сводится к одному вопросу: ресурс изменяется по ходу тестов или остаётся неизменным? Изменяемое готовят заново на каждый тест, дорогое и неизменяемое - один раз.
beforeEach - основной инструмент изоляции: он создаёт свежее состояние перед каждым тестом. Для сервиса это означает новый репозиторий и новый сервис поверх него, так что ни один тест не видит следов предыдущего. Пример с CartService показывает типичную форму: собрать зависимости в beforeEach, а сам тест оставить коротким и сфокусированным на одном действии и его исходе.
afterEach симметричен beforeEach: он убирает ровно то, что тот создал, - закрывает соединение, снимает подписку, восстанавливает подменённые методы. Симметрия важна не ради красоты: несимметричная уборка оставляет открытый handle или подменённый глобал, и следующий тест стартует не с чистого листа. Правило простое - что открыл beforeEach, то закрывает afterEach.
beforeAll оправдан только для дорогого неизменяемого ресурса: например, одно соединение с тестовой БД на весь describe. Держать в beforeAll изменяемое общее состояние - ошибка: такой объект протекает между тестами, накапливает мутации и делает suite зависимым от порядка. Тогда тесты зелёные по одному и красные вместе или наоборот, а причину ищут в шардинге, хотя дело в общем состоянии.
Порядок хуков во вложенных describe строгий. Внешний beforeEach выполняется раньше внутреннего, а afterEach - в обратном порядке: сначала внутренний, потом внешний. При этом тела всех блоков describe исполняются на фазе сбора, до первого теста, поэтому код прямо в describe (а не в хуке) выполнится раньше, чем вы могли ожидать. Держите подготовку в хуках, а не в теле describe.
Отдельная тема - изоляция моков. clearMocks очищает историю вызовов и результатов между тестами, resetMocks вдобавок сбрасывает заданные реализации и возвраты, restoreMocks возвращает оригиналы шпионам и подменённым свойствам. Их включают в конфиге на весь suite или вызывают точечно; удобнее один раз задать политику в конфиге, чем помнить про сброс в каждом файле.
| Настройка | Что делает |
|---|---|
| clearMocks | очищает историю calls/results между тестами |
| resetMocks | также сбрасывает заданные реализации моков |
| restoreMocks | возвращает оригиналы шпионам и свойствам |
Вложенные describe хороши для контекста - 'при пустой корзине', 'для premium-пользователя', - пока не превращаются в лабиринт. Когда состояние теста собрано из цепочки неявных beforeEach в трёх уровнях вложенности, понять его локально уже нельзя: приходится держать всю иерархию в голове. Явный локальный setup почти всегда читается лучше, чем экономия на нём через хитрые наследуемые хуки.
Типичный провал - дорогой beforeAll, который держит изменяемое состояние ради скорости. Экономия оборачивается недетерминизмом: тесты начинают влиять друг на друга и падают в зависимости от порядка или распределения по воркерам. Если ресурс дорогой, но должен быть свежим, делайте дорогую часть один раз в beforeAll, а изменяемую - заново в beforeEach, разделив неизменное и мутируемое.
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 }],
})
})
})