Eager Test.
An Eager Test verifies several distinct methods or behaviors of the unit under test in a single test method, instead of focusing on one behavior.
##Signs and Symptoms
A single test method exercises many unrelated production methods and asserts on each, walking the object through a whole sequence of operations rather than checking one outcome.
Tell-tale signs:
- A vague, catch-all test name (
testUserService,it('works')) or one stitched together with "and" (creates_and_renames_and_deletes). - Multiple act → assert cycles in one body: you call a method, assert, call another, assert again.
- Comments used as section headers inside the test (
// now test delete) to separate the things being checked. - Assertions touch several different methods/fields of the SUT that don't share a single logical outcome.
// SMELL: one test verifies create, rename, delete, and list
test('user service', () => {
const svc = new UserService();
const user = svc.create({ name: 'Ada' }); // behavior 1
expect(user.id).toBeDefined();
svc.rename(user.id, 'Grace'); // behavior 2
expect(svc.get(user.id).name).toBe('Grace');
svc.delete(user.id); // behavior 3
expect(svc.get(user.id)).toBeUndefined();
expect(svc.list()).toHaveLength(0); // behavior 4
});
Note the distinction: an Eager Test is about testing multiple behaviors, not merely having multiple expects. Several assertions on one logical result (e.g. checking several fields of the same returned object) is fine and is not this smell.
##Reasons for the Problem
Why it happens
- Setup reuse / laziness. Arranging the fixture is tedious or expensive, so it's tempting to keep poking at the already-constructed object and assert "while we're here."
- Workflow thinking. The author tests a user journey ("create, then edit, then delete") as one script instead of isolating each unit behavior.
- TDD drift. A test that started focused accumulates extra assertions as new functionality is bolted on rather than given its own test.
Why it hurts
- False confidence (the big one). Most assertion libraries abort the test at the first failing assertion. If
createbreaks, therename,delete, andlistchecks never run — so a green-to-red flip hides how many behaviors are actually broken, and a passing test was never really exercising the later steps once an early one regressed. - Poor diagnostics. A failure tells you "user service test failed," not which behavior. You have to read the whole method to locate the broken step.
- Obscured intent / readability. The test no longer documents a single fact about the system; the reader must mentally segment it into the behaviors it bundles.
- Fragility & coupling. Later asserts depend on earlier mutations, so an unrelated change near the top cascades and makes the test brittle and hard to refactor.
- Maintainability. It's harder to delete, move, or rename a behavior's coverage when it's entangled with three others in one method.
##Treatment
Split the eager test into several focused tests — one behavior per test — and lift the shared arrange step into setup so the split doesn't duplicate boilerplate.
Steps:
- List the behaviors the test bundles (here: create, rename, delete, list-after-delete).
- Extract one test per behavior, each with a descriptive, intention-revealing name.
- Move shared setup into a
beforeEachor a factory/Creation Method so each test still has a clean SUT without copy-pasted arrange code. - Keep a single Act per test and assert only on that behavior's outcome (multiple
expects on the same result are fine). - If you genuinely need to validate an end-to-end workflow, keep that as one explicitly-named scenario/integration test — but still cover each unit behavior in its own test rather than relying on the workflow for coverage.
- Guard against regression by enabling a max-assertions rule (see detectors) as a cheap proxy.
// AFTER: focused tests, shared setup
let svc;
beforeEach(() => { svc = new UserService(); });
test('create() assigns an id', () => {
expect(svc.create({ name: 'Ada' }).id).toBeDefined();
});
test('rename() updates the stored name', () => {
const { id } = svc.create({ name: 'Ada' });
svc.rename(id, 'Grace');
expect(svc.get(id).name).toBe('Grace');
});
test('delete() removes the user', () => {
const { id } = svc.create({ name: 'Ada' });
svc.delete(id);
expect(svc.get(id)).toBeUndefined();
});
Now any single behavior can fail independently, the failing test name pinpoints the break, and each test reads as one documented fact about the SUT.
##Detected by
- eslint-jest jest/max-expects — Enforce a maximum number of expect() calls per test
- eslint-vitest vitest/max-expects — Enforce a maximum number of expect per test