Every test must start with a clean world, otherwise the result begins to depend on run order, and that is the first step toward flakiness. There are four hooks for this: beforeEach and afterEach run around each test, beforeAll and afterAll run once for the whole describe. The choice between them comes down to one question: does the resource change during the tests or stay unchanged? The mutable is prepared anew for each test, the expensive and immutable once.
beforeEach is the main isolation tool: it creates fresh state before each test. For a service that means a new repository and a new service on top of it, so no test sees traces of the previous one. The CartService example shows the typical shape: assemble the dependencies in beforeEach and keep the test itself short and focused on one action and its outcome.
afterEach is symmetric to beforeEach: it removes exactly what the latter created - closes a connection, unsubscribes, restores replaced methods. The symmetry matters not for beauty: asymmetric cleanup leaves an open handle or a replaced global, and the next test does not start from a clean slate. The rule is simple - what beforeEach opened, afterEach closes.
beforeAll is justified only for an expensive immutable resource: for example, one connection to a test database for the whole describe. Holding mutable shared state in beforeAll is a mistake: such an object leaks between tests, accumulates mutations and makes the suite depend on order. Then tests are green one by one and red together or vice versa, and the cause is sought in sharding while it is really shared state.
Hook order in nested describe is strict. The outer beforeEach runs before the inner, and afterEach in reverse: the inner first, then the outer. Meanwhile the bodies of all describe blocks run during the collection phase, before the first test, so code directly in describe (rather than in a hook) runs earlier than you might expect. Keep setup in hooks, not in the describe body.
A separate topic is mock isolation. clearMocks clears the history of calls and results between tests, resetMocks additionally drops the given implementations and return values, restoreMocks returns originals to spies and replaced properties. You enable them in the config for the whole suite or call them pointwise; it is more convenient to set the policy once in the config than to remember to reset in every file.
| Setting | What it does |
|---|---|
| clearMocks | clears calls/results history between tests |
| resetMocks | also drops the given mock implementations |
| restoreMocks | returns originals to spies and properties |
Nested describe blocks are good for context - 'with an empty cart', 'for a premium user' - until they turn into a maze. When a test's state is assembled from a chain of implicit beforeEach across three levels of nesting, you can no longer understand it locally: you have to hold the whole hierarchy in your head. Explicit local setup almost always reads better than saving on it through clever inherited hooks.
The typical failure is an expensive beforeAll that holds mutable state for speed. The saving turns into non-determinism: tests start affecting each other and fail depending on order or distribution across workers. If a resource is expensive but must be fresh, do the expensive part once in beforeAll and the mutable part anew in beforeEach, splitting the immutable from the mutated.
describe('CartService', () => {
let repo: InMemoryCartRepository
let service: CartService
beforeEach(() => {
repo = new InMemoryCartRepository() // fresh state per test
service = new CartService(repo)
})
test('adds an item', async () => {
await service.add('cart-1', product)
await expect(repo.find('cart-1')).resolves.toMatchObject({
items: [{ sku: product.sku, quantity: 1 }],
})
})
})