Managing Test State with Mocha Hooks: Avoiding the Pollution Trap
Mocha hooks extract repeated setup and teardown, but choosing before versus beforeEach determines whether tests stay isolated or pollute each other.
05 May 2026, 21:07 UTC

The repeated setup problem
As a Mocha suite grows, the same initialization code appears before many it blocks. Copying setup for a database connection or a fresh object creates maintenance cost and hides the real assertion.
Mocha hooks extract common arrangement logic. The useful takeaway is to match hook frequency to the cost of the operation and the need for isolation. Using the wrong hook creates test pollution where one test changes state that the next test relies on.
Choosing frequency versus isolation
Mocha provides four hooks that operate on a describe block scope.
One-time hooks before and after
before and after run once per describe block. They are appropriate for expensive, shared resources such as opening a database connection or starting a test server. Because they run once, state created here is shared across all tests in the block.
Per-test hooks beforeEach and afterEach
beforeEach and afterEach run before and after every it block in the current scope. They are the primary tools for test isolation. Resetting a variable, clearing a table, or recreating an object in beforeEach ensures Test A cannot affect Test B.
Hierarchical execution and async flow
Hooks nest with describe blocks. Mocha executes from the outermost scope to the innermost for setup, and reverse for teardown. A global beforeEach runs before a nested beforeEach.
Setup often involves I/O. Mocha supports asynchronous hooks via a callback done, a returned Promise, or async await syntax. Using async await makes the intent clear and lets Mocha wait for the promise to settle before starting the test.
Worked example: User Repository state management
A common pattern is a shared connection for the suite and a clean table for each test.
// Run with: npx mocha test/user-repo.test.js
// Requires Node.js and mocha installed in the project. No elevated permissions needed.
const { db, UserRepo } = require('./app');
const assert = require('assert');
describe('User Repository', function() {
before(async function() {
await db.connect();
});
after(async function() {
await db.disconnect();
});
describe('Create User', function() {
beforeEach(async function() {
await db.table('users').truncate();
});
it('saves a new user to the database', async function() {
const repo = new UserRepo();
await repo.create({ name: 'Alice' });
const user = await repo.findByName('Alice');
assert.strictEqual(user.name, 'Alice');
});
it('fails if the user already exists', async function() {
const repo = new UserRepo();
await repo.create({ name: 'Alice' });
try {
await repo.create({ name: 'Alice' });
assert.fail('Should have thrown');
} catch (err) {
assert.strictEqual(err.message, 'User already exists');
}
});
});
});
Check the result by running the file and confirming both tests pass independently. If beforeEach is removed, the second test will see data left by the first.
Trade-offs and limitations
Overusing before for stateful data leads to pollution because the value persists across tests. A failure in a before hook typically causes all subsequent tests in that block to be skipped.
Deeply nested hooks make execution order hard to trace. Log the hook names with console.log in each hook to verify outer-to-inner order.
Async hooks that never resolve will hang the suite. Always return the promise or use async await, and avoid mixing done callback with async.
Actionable closing
Use before and after for shared, expensive resources. Use beforeEach and afterEach for per-test isolation. Keep hooks shallow and explicit, and verify execution order with simple logs when nesting is required.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.