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

# The Anatomy of a Unit Test

Converting a defect report's reproduction steps into a runnable test, the arrange-act-assert parts of a test body, and the expected value coming from a source independent of the implementation.

The Quality and Testing Fundamentals course closed with a defect's reporting,
prioritization, and the verification of its fix. That course established **what** a test
is and **which** test to write: equivalence classes, boundary values, the test oracle, the
defect report. What remained was a document — steps a person read and carried out by hand.

This course takes up **how** those same steps are written. The "reproduction steps" field
in a defect report is, in effect, the outline of a program: a specific starting state, a
single operation, and an observed result. Once that triple is made runnable, no one has to
check by hand that the defect has not come back. This lesson's question is: what exactly
does that translation look like, and what parts make up a test's body?

## Reproduction Steps

The same domain runs through the course: a small library module that applies library
checkout rules. The rules are written down. Student members may borrow a book for
twenty-eight days, regular members for fourteen. A late return is charged a daily rate, the
first few days are free, and the total fee has a cap. Dates are represented as day numbers;
calendar arithmetic is not this course's subject, and a plain day counter keeps the
examples deterministic.

```js
// library.mjs — version 1
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) {
  const rule = RULES[memberType];
  if (rule === undefined) throw new Error(`unknown member type: ${memberType}`);
  return checkoutDay + rule.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 defect report from a library clerk says: a member who checks out a book on day one
thousand and returns it forty days after the due date is charged a fee of seventy-four
units, even though the written rule caps it at forty. The report's reproduction steps are
three lines, and each line corresponds to a single program statement.

```js
// reproduction.mjs — manual walkthrough of the defect report's steps
import { dueDate, lateFee } from './library.mjs';

const due = dueDate(1000, 'member');
console.log(`due day         : ${due}`);
console.log(`return day      : ${due + 40}`);
console.log(`calculated fee  : ${lateFee(due, due + 40)}`);
console.log(`expected cap: 40`);
```

```
due day         : 1014
return day      : 1054
calculated fee  : 74
expected cap: 40
```

This script reproduces the defect, but it is not a test. A person has to look at the
result — an eye is needed to compare forty against seventy-four. What turns the script into
a test is that the comparison itself enters the program.

## Arrange, Act, Assert

A unit test's body is divided into three parts, and these parts come in the same order in
every test.

The **arrange** part sets up the starting state the operation will run against: inputs,
objects, dependencies. The **act** part calls the operation under test **exactly once**.
The **assert** part compares the observed result with the expected one and fails the test
on a mismatch.

```js
// late-fee.test.mjs — the defect report's reproduction steps
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { dueDate, lateFee } from './library.mjs';

test('late fee does not exceed the cap', () => {
  const due = dueDate(1000, 'member');

  const fee = lateFee(due, due + 40);

  assert.equal(fee, 40);
});
```

The three parts are separated by blank lines; a separate comment line is not needed. The
value of this layout is not aesthetic. The act part being a single line states, without
ambiguity, **which** call the test is exercising. With two calls, a failing test does not
say which one failed. A lengthening arrange part is a separate signal: it shows the unit
under test depends on too many things. The fifth lesson measures that signal.

Tests run with the Node runtime's built-in test runner. The runner's verbose output also
includes duration, file paths, and process information, which vary with the environment.
Throughout the course, only the result lines are filtered out, because those are what
teach.

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

```
not ok 1 - late fee does not exceed the cap
# tests 1
# pass 0
# fail 1
```

The test failed, and this is the expected result: the defect has not been fixed yet. A
failing run is proof that the test actually tests something. A test that has never failed
cannot say whether it is green because it is correct or because it was written wrong.

## Where the Expected Value Comes From

The number forty in the assert part came from the written rule. This detail carries the
test's entire value. When the expected value is derived from the implementation itself, the
test becomes a **tautology**: it confirms that what the code does is what the code does.

```js
// tautological.test.mjs — a test that computes the expected value the same way as the implementation
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { LATE_FEE, lateFee } from './library.mjs';

test('late fee is calculated correctly', () => {
  const daysLate = 40;
  const expected = (daysLate - LATE_FEE.graceDays) * LATE_FEE.dailyRate;

  assert.equal(lateFee(1014, 1014 + daysLate), expected);
});
```

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

```
ok 1 - late fee is calculated correctly
# tests 1
# pass 1
# fail 0
```

This test came back green on the code the defect **stands** in. The formula producing the
expected value is the same formula inside the function under test; both forget the cap.
The test oracle concept introduced in the Quality and Testing Fundamentals course names
exactly this point: the source of the expected result must be **independent** of the code
under test. That source can be a written rule, a calculation done by hand, a known
reference value, or a result produced some other way — but it cannot be the tested
function's own logic.

## The Fix and Comparing Two Tests

The fix is a single line: take the smaller of the raw fee and the cap.

```js
// library.mjs — version 2: cap is applied
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) {
  const rule = RULES[memberType];
  if (rule === undefined) throw new Error(`unknown member type: ${memberType}`);
  return checkoutDay + rule.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);
}
```

When the two test files are run together, both of their outcomes reverse.

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

```
ok 1 - late fee does not exceed the cap
not ok 2 - late fee is calculated correctly
# tests 2
# pass 1
# fail 1
```

The rule-bound test turned green on the correct behavior. The implementation-bound test,
on the other hand, failed on **correct** code. If a test fails when the code is fixed, what
it tests is not behavior. This is the plainest form of the flakiness that is the eighth
lesson's subject; here it is enough just to name it.

## The Boundary of a Unit

The word "unit" names a **boundary**, not a size. A unit test sets up a context in which
nothing outside the code under test affects the result. In practice this means keeping
three things out: out-of-process resources such as disk and network, the system clock and
sources of randomness, and state left behind by other tests.

This constraint is not arbitrary. Every resource kept out adds a share to the test's
duration and stability. The test above runs in under a millisecond, and its result does not
depend on the machine, the clock, or run order. If the same test had to write to a
database, a failing run could be caused by the code, the configuration, the network, or
leftover data — choosing among four possibilities is far more expensive than reading a
single one.

The `dueDate` call staying in the arrange part is an example of this boundary. The unit
under test is the `lateFee` function; `dueDate` only produces input. Even though both are
in the same file, the test's intent is single, and a failing run points to one function.

## Summary

- A defect report's reproduction steps can be converted into a runnable test; what
  completes the conversion is that the comparison itself enters the program.
- A test body consists of arrange, act, and assert parts; the act part being a single call
  removes ambiguity about the source of a failing run.
- A failing run is proof that the test actually tests something; a test that has never
  failed cannot say why it is green.
- The expected value must come from a source independent of the code under test; a test
  that repeats the implementation's formula also comes back green on code the defect
  stands in.
- A unit is a boundary, not a size: out-of-process resources, the clock, randomness, and
  state left behind by other tests all stay outside that boundary.

## Next Step

This lesson used a single assertion: `assert.equal`. That was enough for the equality of
two numbers, because the function under test returned a number. When a loan operation
returns an object, an error, or a list, an equality check either does not work or measures
the wrong thing. The next lesson takes up assertion choice: which assertion catches which
defect, how much signal an overly broad assertion loses, and how that loss is measured.
