Mocha Hooks: Use Scoping to Keep Tests Isolated Without Paying for Setup Every Time
Mocha hooks control test setup and teardown. Match before/after to suite lifetime and beforeEach/afterEach to test isolation to avoid pollution and unnecessary cost.
30 Jan 2026, 22:16 UTC

Tests that pass alone and fail together usually mean shared state is leaking between cases. In Mocha, hooks — before, after, beforeEach, afterEach — are the control surface for setup and teardown. The useful takeaway is not to use them for convenience, but to match the hook scope to the lifetime of the resource you are managing.
The problem is lifetime mismatch
A common concrete problem is a service client that needs a connection once per suite, but each test needs a clean in-memory store. Putting the store reset in a root before hook makes the first test see a clean store and every later test see whatever the previous test left behind. That is test pollution.
Mocha runs hooks in a predictable hierarchy. Root-level hooks defined outside any describe run for every test in the file. Hooks inside a describe run for tests in that block and its children. Outer hooks run before inner hooks, and after/afterEach run in reverse order.
Scoping hooks to the right describe
Think of before/after as suite-level and beforeEach/afterEach as test-level.
before runs once before the first test in its scope. It is appropriate for expensive, immutable setup such as starting a test database container or loading fixtures that you do not mutate.
beforeEach runs before every test in its scope. It is the primary mechanism for test isolation: resetting mocks, clearing module caches, or re-instantiating objects so one test cannot affect another.
Asynchronous hooks are supported by returning a Promise or accepting a done callback. Mocha will wait for the Promise to settle before running the next hook or test.
Worked example: hierarchical setup for an API client
The following structure shows how scope changes behavior. Create test/hooks.example.js in a Node project with Mocha installed as a dev dependency.
// test/hooks.example.js
const assert = require('assert');
describe('API client', function() {
let client;
before(function() {
// Runs once for this describe. Suitable for expensive, shared init.
// e.g., start test server
});
beforeEach(function() {
// Runs before each test. Suitable for isolation.
client = { store: {} };
});
afterEach(function() {
// Cleanup per test, e.g., clear timers
});
describe('users', function() {
before(function() {
// Runs once before user tests, after outer before
});
it('creates a user', function() {
client.store.user = 'alice';
assert.strictEqual(client.store.user, 'alice');
});
it('sees empty store', function() {
// With beforeEach, store is fresh. Without it, this would see 'alice'.
assert.deepStrictEqual(client.store, {});
});
});
after(function() {
// Runs once after all tests in this describe
});
});
Run from the project root with no special permissions:
npx mocha test/hooks.example.js
To verify execution order, add a console.log to each hook. The order follows the nesting: outer before, inner before, beforeEach, test, afterEach, then unwind.
Trade-offs and limits
Overusing shared mutable state in before hooks creates pollution where one test failure causes later tests to fail. Excessive nesting of describe blocks with hooks in each level makes the execution order hard to trace.
Heavy logic in beforeEach guarantees isolation but increases total suite time linearly with test count. For a resource-intensive setup, prefer before for the immutable part and beforeEach only for the cheap reset.
A practical way to check the result is to temporarily introduce a mutation in a test and observe whether a sibling test is affected. If it is, the isolation boundary is too wide.
Actionable closing
Audit hooks by scope: move expensive, read-only setup to the highest before that is still safe, and keep all mutable resets in the narrowest beforeEach. Keep describe nesting shallow enough that the hook order can be read without a diagram. That gives you fast suites and tests that remain independent.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.