---
title: 'Test Naming'
source: 'https://academia.sh/en/courses/unit-testing/test-naming'
course: 'Unit Testing and Test-Driven Development'
language: en
updated: '2026-08-23T14:25:21+00:00'
license: 'CC BY-SA 4.0'
---

# Test Naming

Building a test name from its unit, condition, and expected-result parts, how bundling three rules into one test makes the name misleading, and how the names of passing tests form a readable list of rules.

The first piece of information a failing run gives is not the assertion, it is the name.
In the line the runner reports, the result comes first, then the test's name; the detailed
error message sits below that. If the name states the rule, the reader usually knows what
broke without going to the source. If the name says something like "record is correct",
reading the error message and going from there to the source becomes mandatory.

This lesson takes the name out of being a matter of style and ties it to something
measurable: when the same three rules are written in two different forms, how much does the
information a failing run gives change?

## A Name's Three Parts

A good test name is a sentence and carries three parts: which **unit**, under which
**condition**, gives which **result**. When all three are present, the name alone is a
readable rule.

In the sentence `a student loan lasts twenty-eight days`, the unit is the loan-duration
calculation, the condition is the member type being a student, and the result is
twenty-eight days. By contrast, the name `dueDate test` has neither a condition nor a
result; the name `works correctly` does not even have a unit.

Two application rules keep this shape. A name refers not to the steps **inside** the
function under test, but to the behavior observed from outside: `Math.min is called` is an
implementation detail, `does not exceed the cap` is a behavior. A name states a positive
rule instead of a negation: `accepts a loan within the limit` instead of `does not throw`.

## Three Rules in One Test

In the library version below, three rules have regressed at once: the student loan
duration has dropped to fourteen days, the grace period has been zeroed out, and the late
fee's cap has been removed.

```js
// rules.mjs — regressed version: three rules broken at once
export const RULES = {
  student: { loanDays: 14, maxLoans: 10 },
  member: { loanDays: 14, maxLoans: 5 },
};

export const LATE_FEE = { graceDays: 0, dailyRate: 2, cap: 40 };

export function dueDate(checkoutDay, memberType) {
  return checkoutDay + RULES[memberType].loanDays;
}

export function lateFee(dueDay, returnDay) {
  const daysLate = returnDay - dueDay;
  if (daysLate <= LATE_FEE.graceDays) return 0;
  return (daysLate - LATE_FEE.graceDays) * LATE_FEE.dailyRate;
}
```

The form that bundles three rules into one test can only say something generic in its
name.

```js
// combined.test.mjs — the same three rules verified in a single test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { dueDate, lateFee } from './rules.mjs';

test('loan rules work correctly', () => {
  assert.equal(dueDate(1000, 'student'), 1028);
  assert.equal(lateFee(1000, 1003), 0);
  assert.equal(lateFee(1000, 1100), 40);
});
```

```sh
node --test --test-reporter=tap combined.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - loan rules work correctly
# tests 1
# pass 0
# fail 1
```

Three rules are broken, the report is one line. The reason is not naming alone: when one
assertion fails, the rest of the test never runs. The second and third assertions were
never evaluated, so the state of those two rules is unknown. As fixes are made, the same
test fails again each time, and each round surfaces the next rule.

The same three rules, split into separate tests, produce a different report.

```js
// separate.test.mjs — the same three rules, each in its own test
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { dueDate, lateFee } from './rules.mjs';

test('a student loan lasts twenty-eight days', () => {
  assert.equal(dueDate(1000, 'student'), 1028);
});

test('a return within the grace period is free', () => {
  assert.equal(lateFee(1000, 1003), 0);
});

test('late fee does not exceed the cap', () => {
  assert.equal(lateFee(1000, 1100), 40);
});
```

```sh
node --test --test-reporter=tap separate.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
not ok 1 - a student loan lasts twenty-eight days
not ok 2 - a return within the grace period is free
not ok 3 - late fee does not exceed the cap
# tests 3
# pass 0
# fail 3
```

Same code, same three rules, same three assertions. The difference is measurable: the
first form reported that one rule was broken; the second form reported that three rules
were broken, and which ones. The information obtained without looking at the source
tripled.

This is the companion half of the second lesson's signal-strength measurement. There, how
many corruptions an assertion **catches** was measured; here, how much of a caught
corruption **reaches** the report is measured. The rule that a test verifies one behavior
is the joint result of these two measurements, not an aesthetic preference.

## The Sum of the Names Is a Specification

Once the rules are fixed, the list of passing tests serves another purpose: it describes
the library's behavior in readable form.

```js
// rules.mjs — fixed version
export const RULES = {
  student: { loanDays: 28, maxLoans: 10 },
  member: { loanDays: 14, maxLoans: 5 },
};

export const LATE_FEE = { graceDays: 3, dailyRate: 2, cap: 40 };

export function dueDate(checkoutDay, memberType) {
  return checkoutDay + RULES[memberType].loanDays;
}

export function lateFee(dueDay, returnDay) {
  const daysLate = returnDay - dueDay;
  if (daysLate <= LATE_FEE.graceDays) return 0;
  const raw = (daysLate - LATE_FEE.graceDays) * LATE_FEE.dailyRate;
  return Math.min(raw, LATE_FEE.cap);
}
```

Grouping tests under topic headings splits the load the name would otherwise carry: the
unit and the context go to the group, the condition and the result stay with the test.

```js
// specification.test.mjs — a test suite whose names read as a list of rules
import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { dueDate, lateFee } from './rules.mjs';

describe('loan duration', () => {
  test('twenty-eight days for a student member', () => {
    assert.equal(dueDate(1000, 'student'), 1028);
  });

  test('fourteen days for a standard member', () => {
    assert.equal(dueDate(1000, 'member'), 1014);
  });
});

describe('late fee', () => {
  test('zero within the grace period', () => {
    assert.equal(lateFee(1000, 1003), 0);
  });

  test('charged per day outside the grace period', () => {
    assert.equal(lateFee(1000, 1008), 10);
  });

  test('does not exceed the cap', () => {
    assert.equal(lateFee(1000, 1100), 40);
  });
});
```

```sh
node --test --test-reporter=tap specification.test.mjs | grep -E '^ *(ok|not ok|# (tests|pass|fail))'
```

```
    ok 1 - twenty-eight days for a student member
    ok 2 - fourteen days for a standard member
ok 1 - loan duration
    ok 1 - zero within the grace period
    ok 2 - charged per day outside the grace period
    ok 3 - does not exceed the cap
ok 2 - late fee
# tests 5
# pass 5
# fail 0
```

The indented lines give the individual rules, the unindented lines give the groups. Read
from top to bottom, the output turns into a list of rules: the loan duration is
twenty-eight days for a student member, fourteen for a standard member; the late fee is
zero within the grace period, charged per day outside it, and does not exceed the cap.

This list is not the written specification itself, but it can be compared against it. A
rule present in the specification but missing from the list is the traceability gap named
in the What Is Quality Assurance lesson in the Quality and Testing Fundamentals course. A
line present in the list but missing from the specification is a warning in the opposite
direction: either the specification was written incompletely, or the test has locked in a
behavior nobody asked for.

## Alignment Between Name and Assertion

There is a single consistency rule between name and assertion: every field named in the
assertion must be part of the rule the name states. If a test named `twenty-eight days for
a student member` also checks the record's status field, that test is testing two rules,
and its name is hiding the second one.

The reverse direction is equally binding: if the rule the name states does not appear in
the assertion, the test is not living up to its name. The second lesson's unmatched error
expectation was an example of this — its name spoke of the loan limit, but what it
actually tested was a missing field.

When an assertion grows without the name changing, this alignment breaks silently. The
question to ask when adding a new assertion to a test is this: is this assertion within the
existing name's scope, or is a new test needed? The second answer is usually the right one,
and it costs three lines.

## Summary

- A test name carries its unit, condition, and expected-result parts; when all three are
  present, the name alone is a readable rule.
- A name refers to the behavior observed from outside, not to implementation steps, and it
  states a positive rule instead of a negation.
- When one assertion fails, the test's remaining assertions never run; the form that
  bundled three rules into one test reported one failure, and the split form reported
  three.
- Read from top to bottom, the names of passing tests give a list of rules; this list can
  be compared against the written specification.
- Every field named in the assertion must be within the name's scope; when an assertion
  grows without the name changing, the test starts testing a rule its name does not state.

## Next Step

Up to this point, every test looked only at its own input, and every run gave the same
result. As tests multiply, this does not hold on its own: two tests sharing a common
fixture see the state the other left behind, and run order starts to determine the result.
The next lesson produces this bond with a real run — the same tests give different results
in a different order — and builds the setup that keeps tests fast and independent of each
other.
