Using beforeAll, beforeEach, afterEach, and afterAll hooks for test setup and teardown, understanding execution order, scope with describe blocks, and preventing test pollution.
beforeAll runs once before all tests, beforeEach before each test, afterEach after each test, and afterAll once after all tests. All support async operations.
Hooks are scoped to their describe block. Outer hooks run before inner hooks, allowing layered setup from general to specific state.
Use beforeEach for fresh setup and afterEach for cleanup to prevent one test's side effects from affecting others. Clear mocks, DOM, and global state consistently.
Use beforeAll for expensive shared setup (database connections, server startup). Use beforeEach for state that must be fresh per test (mock resets, test data creation).
Test lifecycle hooks control setup and cleanup operations that run at specific points during test execution. Proper use of these hooks prevents test pollution (where one test's state affects another) and keeps test code DRY. Both Jest and Vitest use the same API for lifecycle hooks.
beforeAll(fn): Runs once before all tests in the current describe block (or file if not inside a describe). Use for expensive setup that can be shared across tests:
beforeEach(fn): Runs before each individual test. Use for setup that must be fresh for every test:
afterEach(fn): Runs after each individual test. Use for cleanup that prevents test pollution:
afterAll(fn): Runs once after all tests in the block complete. Use for expensive teardown:
Understanding the execution order is critical for interviews:
beforeAll(() => console.log('1 - beforeAll'));
afterAll(() => console.log('6 - afterAll'));
beforeEach(() => console.log('2 - beforeEach'));
afterEach(() => console.log('4 - afterEach'));
test('first test', () => console.log('3 - test'));
// Output: 1, 2, 3, 4, 2, 3, 4, 6 (for two tests)Lifecycle hooks are scoped to their describe block. Outer hooks run before inner hooks:
describe('outer', () => {
beforeEach(() => console.log('outer beforeEach'));
describe('inner', () => {
beforeEach(() => console.log('inner beforeEach'));
test('example', () => {});
// Runs: outer beforeEach -> inner beforeEach -> test
});
});This scoping lets you set up common state at the outer level and specific state at the inner level.
Test pollution is when one test's side effects affect another test's behavior, causing intermittent failures that are difficult to debug. Common sources:
The solution is consistent use of beforeEach for fresh setup and afterEach for cleanup:
beforeEach(() => {
jest.clearAllMocks(); // or vi.clearAllMocks()
localStorage.clear();
});
afterEach(() => {
cleanup(); // RTL's cleanup (automatic in most setups)
});All lifecycle hooks support async operations. Return a promise or use async/await:
beforeAll(async () => {
await database.connect();
});
afterAll(async () => {
await database.disconnect();
});beforeAll instead of beforeEach, causing shared mutable stateafterEach, leading to flaky testsbeforeAll for mock setup that should reset between testsPrefer inline setup for simple tests -- it makes tests self-contained and readable. Use lifecycle hooks when setup is expensive or when the same setup is repeated across many tests in a describe block.
Fun Fact
The beforeAll/afterAll pattern in testing was borrowed from database transactions. In database testing, you open a transaction in beforeAll and roll it back in afterAll, making the entire test suite run against a consistent database snapshot without persisting any changes.