Tests live in one process, run one after another, and share memory. That gives two ways to lie. The first is leaked state: one test mutates a shared object, and the next passes or fails for a reason that is not its own. The second is unfinished asynchrony: the check runs before the work has finished. Both disciplines, a clean world and honest waiting, serve determinism: the outcome depends on the code, not on run order and a race.
A clean world means each test starts from a freshly built environment. Take CartService on top of InMemoryCartRepository: in beforeEach we create a fresh repository and a new service before every test. Then one test never sees items added by another, and any test can run alone with the same result. The rebuild is cheap, yet the tests stay independent: a failure points at the code, not at a neighbour in the same file.
The temptation is to build the service once in beforeAll: it seems faster. But the moment even one test mutates the shared repository, the state leaks into the next ones, and the suite becomes order-dependent: green as a whole, yet failing if you run a single test or change their order.
beforeAll is justified for an expensive, immutable resource: spin up a database container or a large fixture file. As soon as mutable state is involved, go back to beforeEach. afterEach is for symmetric cleanup - close a connection, remove a listener, restore a global setting.
Nested describe blocks are handy while they set context: cart, empty cart, cart with an item. The danger is elsewhere: when there are many hooks scattered across nesting levels, a test assembles its environment from implicit beforeEach blocks somewhere above, and it is no longer clear which state you are dealing with.
Mocks are shared state too, and they must be cleaned between tests. clearMocks clears the history of calls and results. resetMocks does the same and additionally drops the configured implementations and return values. restoreMocks restores the originals for spies and replaced properties. In Jest 30 all three are off by default, so mock isolation is turned on explicitly.
| Setting |
|---|
| What it does |
|---|
| clearMocks | clears the calls/results history |
|---|---|
| resetMocks | also drops mock function implementations |
| restoreMocks | restores the originals of spies/replaced properties |
Asynchrony adds a second way to lie, and the most common false green in Jest is a forgotten return or await on the promise that carries the assertion. If a test starts a promise but neither returns nor awaits it, Jest sees the synchronous part finish without error and marks the test passed. Meanwhile the expect inside .then runs later - already inside someone else's test, or never at all. The fix is simple: a test must return the promise or be async and await the work, so that Jest waits for the real result.
For promises there are direct matchers: await expect(load()).resolves.toMatchObject(...) waits for a resolution, while await expect(p).rejects.toThrow(...) checks a rejection with the right error - always with await, or you get a floating promise again. When a check hides inside a conditional branch or a catch, add a guard: expect.assertions(1) demands exactly one assertion, expect.hasAssertions() at least one, and the test fails if the expected expect never ran. And drop the legacy done callback: it swallows errors thrown inside then, and a forgotten done call turns a failure into a timeout.
One more trap is an unhandled promise rejection: it surfaces asynchronously and is often blamed on the next test, so the red lights up where the error is not. Catch rejections where you expect them, rather than leaving them dangling. In the end: without a clean world and honest waiting, a suite can be green while proving nothing, and sooner or later it will flicker flaky.
describe('CartService', () => {
let repo: InMemoryCartRepository
let service: CartService
beforeEach(() => {
repo = new InMemoryCartRepository()
service = new CartService(repo)
})
test('adds a product', 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('rejects an unknown user', async () => {
expect.assertions(1)
await expect(loadUser('missing')).rejects.toThrow('User not found')
})