Setup и teardown - это код вокруг теста: что-то приготовить до, что-то прибрать после. Jest даёт четыре хука. beforeAll выполняется один раз перед всеми тестами блока, afterAll - один раз после всех; beforeEach - перед каждым тестом, afterEach - после каждого. Вся суть в частоте. На примере checkout-сервиса: соединение с тестовой базой дорого поднимать, его открывают один раз в beforeAll; корзину каждый тест должен получить свежей, её собирают в beforeEach.
| Хук | Когда выполняется | Для чего |
|---|---|---|
| beforeAll | один раз перед всеми тестами блока | дорогой неизменяемый ресурс: соединение с БД, контейнер |
| beforeEach | перед каждым тестом | свежее изменяемое состояние: корзина, запись пользователя |
| afterEach | после каждого теста | откат того, что настроил beforeEach |
| afterAll | один раз после всех тестов | закрытие того, что открыл beforeAll |
Правило простое: в beforeAll кладут дорогое и неизменяемое - соединение с базой, поднятый контейнер, прочитанный фикстур. В beforeEach - то, что каждый тест должен иметь в чистом, известном состоянии: новую корзину, свежую запись пользователя. Смешивать нельзя: положите в beforeAll изменяемую корзину - первый же тест, добавивший товар, испортит вход второму.
Уборка обязана быть симметрична подготовке: что открыл beforeAll, закрывает afterAll; что настроил beforeEach, откатывает afterEach. Открыли соединение - в afterAll закрываем, иначе процесс Jest повиснет на открытом хэндле. Заполнили таблицу в beforeEach - в afterEach чистим, иначе строки одного теста просочатся в следующий. Симметрия - это гарантия: после любого теста мир возвращается в исходное состояние, и порядок запуска перестаёт влиять на результат.
Порядок хуков во вложенных describe строг. Внешний beforeEach идёт раньше внутреннего, а afterEach - в обратном порядке: сначала внутренний, потом внешний. beforeAll внешнего блока предшествует beforeAll внутреннего, afterAll - наоборот. Для теста во вложенном блоке это внешний beforeEach, затем внутренний, сам тест, внутренний afterEach и, наконец, внешний afterEach.
Здесь классическая ловушка. Jest сначала выполняет тела всех describe целиком - это фаза сбора, где он лишь регистрирует тесты и хуки, - и только потом запускает сами тесты. Значит, код в теле describe, а не в хуке или тесте, отработает раньше всех beforeAll и всех тестов, причём для всех блоков сразу. Отсюда баг: значение вычисляют прямо в describe, рассчитывая, что оно готово к тесту, а оно посчиталось на фазе сбора, до beforeAll, который должен был подготовить данные. Место такой подготовки - хук, а не тело describe.
Отсюда компромисс между общим setup и свежестью. beforeEach даёт независимость: каждый тест стартует с чистого листа, но платит пересборкой каждый раз. beforeAll даёт скорость: тяжёлый ресурс поднят однажды, но требует, чтобы тесты его не меняли. Признак выбора один: мутируется ресурс тестами или нет.
Провал случается, когда дорогой beforeAll держит изменяемое общее состояние. Ради скорости корзину создали один раз в beforeAll. Тест 'добавляет товар' кладёт позицию и проходит. Следующий 'пустая корзина показывает ноль' ждёт пустоту, но видит товар от предыдущего и падает - или, хуже, проходит по совпадению. В одиночку тест зелёный, в составе файла - красный. Набор зависит от порядка, а не от поведения, и позже это проявляется как flaky.
Изоляция моков из соседней главы - частный случай той же симметрии для другого вида общего состояния. Там речь о политике сброса - clearMocks/resetMocks и когда их применять; здесь - о самих примитивах жизненного цикла, на которых политика держится. Сброс моков вызывают из beforeEach или включают конфигом, который вешает тот же хук. Освоив четыре хука и их порядок, вы чистите любое общее состояние - объекты, моки, ресурсы, - и тест снова зависит только от кода.
describe('checkout', () => {
let db: TestDb
let cart: Cart
beforeAll(async () => {
db = await connectTestDb() // дорого, поднимаем один раз
})
afterAll(async () => {
await db.close() // закрываем открытый ресурс
})
beforeEach(() => {
cart = new Cart() // свежая корзина на каждый тест
})
afterEach(async () => {
await db.reset() // откатываем изменения теста
})
test('добавляет товар в корзину', () => {
cart.add(product)
expect(cart.total).toBe(product.price)
})
})beforeAll(() => console.log('1 - beforeAll'))
afterAll(() => console.log('1 - afterAll'))
beforeEach(() => console.log('1 - beforeEach'))
afterEach(() => console.log('1 - afterEach'))
test('', () => console.log('1 - test'))
describe('nested', () => {
beforeAll(() => console.log('2 - beforeAll'))
afterAll(() => console.log('2 - afterAll'))
beforeEach(() => console.log('2 - beforeEach'))
afterEach(() => console.log('2 - afterEach'))
test('', () => console.log('2 - test'))
})
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAlldescribe('outer', () => {
console.log('describe outer-a')
describe('inner 1', () => {
console.log('describe inner 1')
test('test 1', () => console.log('test 1'))
})
console.log('describe outer-b')
test('test 2', () => console.log('test 2'))
describe('inner 2', () => {
console.log('describe inner 2')
test('test 3', () => console.log('test 3'))
})
console.log('describe outer-c')
})
// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test 1
// test 2
// test 3