Unit Testing: What It Is For
Chapter Thirty-Three
Syllabus topic Module 1, "Software Testing Strategies: Unit Testing: purpose"
Pages 183 to 187 of 622
In one line
Unit testing checks each smallest testable piece of a program on its own, as soon as it is written, so that a mistake is found where it was made, while it is cheap to fix and before other code is built on it.
In the wording a student can write in an examination: a unit is a "separately testable element specified in the design of a computer software component" (ISO/IEC/IEEE 24765); a unit test is the "testing of individual routines and modules by the developer or an independent tester" (ISO/IEC TR 7052:2023). The ISTQB syllabus calls the level component testing, which "focuses on testing components in isolation" and "is normally performed by developers in their development environments". Its purpose is to find defects in each unit's own logic early and in one known place; to check the unit's interface, local data, boundaries, paths and error handling; and to leave behind tests that can be rerun after every change.
What a unit is
The standards give three angles on the word. A unit is a "separately testable element specified in the design of a computer software component", a "logically separable part of a computer program", and a "software element that is not subdivided into other components or elements" (all ISO/IEC/IEEE 24765). ISO/IEC/IEEE 12207:2026 calls it a software unit: an "atomic-level software component of the software architecture that can be subjected to standalone testing".
The key phrase in every version is separately testable. In Python a unit is usually a function or a class; in Java a class or a method; in a web application a single request handler or service. ExamReg's module tree from earlier chapters gives natural units: the password check, the eligibility check, the paper selection, the fee calculator, the payment gateway adapter, the seat allocation and the PDF generator. Each can be called on its own, given inputs, and checked.
What unit testing is for
Unit testing earns its place in a strategy for four reasons.
- It finds defects where they are made. When a unit test fails, the fault is in that unit, a few dozen lines at most. When a system test fails, the fault could be anywhere, as Chapter Thirty-One, on the strategic approach, showed.
- It finds them early, when they are cheapest. Boehm and Basili's first finding was that "Finding and fixing a software problem after delivery is often 100 times more expensive than finding and fixing it during the requirements and design phase." They added a qualification that fits a project of ExamReg's size: for "small, noncritical software systems" the factor is "more like 5:1 than 100:1". Either way, the unit test is the earliest dynamic test there is.
- It lets the code be changed safely. A unit's tests stay with it, and rerunning them after every change is the cheapest regression testing a project has.
- It improves the design. A unit that is hard to test on its own is usually doing too much or depending on too much; writing its tests exposes that, which is why testability is a design property (Chapter Twenty-One, on how quality factors shape testing).
Unit Testing: What It Is For
The standards stress one more thing: unit tests are normally written by the developer who wrote the unit. The ISTQB syllabus says component testing "is normally performed by developers in their development environments", and ISO/IEC TR 7052 allows "the developer or an independent tester". It is the one level where the author is expected to test their own work, because no one else knows the unit's inside as well, and the tests are needed long before a test team sees the code.
What a unit test checks
A useful checklist for testing one unit has five parts.
| Aspect | The question | For ExamReg's fee calculator |
|---|---|---|
| Interface | Does the unit accept what its callers send, and return what they expect? | Takes days late, backlog papers and a concession flag; returns a whole number of rupees |
| Local data | Are its own variables set, updated and used correctly? | The running fee starts at the form fee plus backlog fees and is reduced only by the concession |
| Boundaries | Is every edge right, at and just either side of it? | 0, 7, 8, 15 and 16 days late; zero backlog papers |
| Paths | Is every branch of the unit taken at least once? | With and without a concession; each late-fee band; the refusal |
| Error handling | Is bad input refused cleanly, with a clear message? | Negative days, negative backlog papers, anything that is not a whole number |
Each aspect has a chapter of its own later. Boundaries become boundary value analysis (Chapter Fifty-Five); paths become branch testing and, made exact, basis path testing (Chapter Seventy-Two); interfaces between units become integration testing (Chapter Thirty-Seven).
Worked example: unit testing the fee calculator
Here is ExamReg's fee calculator, with the fee rules fixed for this book: a form fee of Rs 800, Rs 150 for each backlog paper, the late-fee bands, forms more than 15 days late or with a negative number of days refused, and a concession that waives the form fee only. The checks below take the five aspects in turn. Each is a line of ordinary Python that calls the unit with chosen inputs and compares the answer with the rule; Chapter Thirty-Five, on writing unit tests with a framework, shows the same checks as a proper test suite.
FORM_FEE, BACKLOG_FEE = 800, 150
def total_fee(days_late, backlog_papers, concession): # the unit under test
if days_late < 0 or days_late > 15:
raise ValueError("form not accepted")
fee = FORM_FEE + BACKLOG_FEE * backlog_papers
if concession:
fee -= FORM_FEE # a concession waives the form fee
if days_late == 0:
late = 0
elif days_late <= 7:
late = 100
else:
late = 500
return fee + late
def refused(*args):
try:
total_fee(*args)
except ValueError:
return True
return False
checks = {
"interface: returns a whole number of rupees": isinstance(total_fee(0, 0, False), int),
"local data: form fee plus two backlog papers": total_fee(0, 2, False) == 1100,
"boundary: 7 days late pays Rs 100 late fee": total_fee(7, 0, False) == 900,
"boundary: 8 days late pays Rs 500 late fee": total_fee(8, 0, False) == 1300,
"boundary: 15 days late is accepted": total_fee(15, 0, False) == 1300,
"boundary: 16 days late is refused": refused(16, 0, False),
"path: no concession, on time": total_fee(0, 0, False) == 800,
"path: concession, 3 days late, 2 backlog papers": total_fee(3, 2, True) == 400,
"error handling: negative days refused": refused(-1, 0, False),
"error handling: negative backlog papers refused": refused(0, -2, False),
}
for name, passed in checks.items():
print("pass" if passed else "FAIL", " ", name)
print(f"{sum(checks.values())} of {len(checks)} checks passed;",
"total_fee(0, -2, False) returned", total_fee(0, -2, False))Unit Testing: What It Is For
pass interface: returns a whole number of rupees
pass local data: form fee plus two backlog papers
pass boundary: 7 days late pays Rs 100 late fee
pass boundary: 8 days late pays Rs 500 late fee
pass boundary: 15 days late is accepted
pass boundary: 16 days late is refused
pass path: no concession, on time
pass path: concession, 3 days late, 2 backlog papers
pass error handling: negative days refused
FAIL error handling: negative backlog papers refused
9 of 10 checks passed; total_fee(0, -2, False) returned 500Nine checks pass, and the one that fails is exactly the kind a unit test exists to catch. The calculator validates days late but never validates backlog papers, so a student record corrupted to hold -2 backlog papers produces a bill of Rs 500: the form fee of Rs 800 less Rs 300 for "negative" papers. No end-to-end system test is likely to try that input, because the form's own screen would never offer it; but a unit can be called by any other code, including a data import or a future screen, and it has to defend itself. The fix is one line at the top of the unit, refusing a negative or non-whole number of backlog papers as it already refuses bad days, and the failing check becomes the confirmation test for the fix.
Three things about the example are true of unit testing in general. The tests ran in a fraction of a second, so they can run every time the code is saved. The failure named the unit and the input exactly, so there was nothing to debug but one condition. And the checks stay with the code, so the next person to change total_fee inherits a record of what it must do.
Unit Testing: What It Is For
What it does not mean
Unit testing is not debugging. The test shows that the unit fails for -2 backlog papers; finding and fixing the missing check is debugging, a separate activity (Chapter Forty-Seven).
Passing unit tests do not show that the units work together. The fee calculator can pass all its tests while the payment adapter misreads the total it returns; that is integration testing's job.
A unit is not always one function. It is the smallest separately testable element, which may be a class or a small module.
Unit tests are not a chore to add at the end. Written as the unit is written, or before it as the next chapters show, they cost least and find most.
Quick revision
- Unit: a separately testable element of a software component; software unit: an atomic-level component that can be tested standalone.
- Unit test: testing of individual routines and modules, usually by the developer, in the development environment.
- Purposes: find defects where they are made and early; enable safe change; improve design and testability.
- Late fixes are "often 100 times more expensive" than early ones, "more like 5:1" for small, noncritical systems (Boehm and Basili 2001).
- Checklist: interface, local data, boundaries, paths, error handling.
- Worked example: nine of ten checks passed; the unit accepted -2 backlog papers and billed Rs 500.
Test yourself
1. What is a unit, and what is unit testing? A unit is the smallest separately testable element of a program, such as a function or a class. Unit testing is the testing of individual units in isolation, normally by the developer in the development environment, to find defects in each unit's own logic.
2. State four purposes of unit testing. To find defects where they are made, in a small known place; to find them early, when fixing is cheapest; to provide tests that can be rerun after every change as regression tests; and to expose design problems, since a unit that is hard to test alone is usually doing too much.
3. What aspects of a unit should its tests check? Its interface (what it accepts and returns), its local data (its own variables), its boundaries (every edge and either side of it), its paths (every branch at least once), and its error handling (bad input refused cleanly).
4. Why are defects found by unit tests cheaper to fix than those found later? Because a failing unit test points to one small unit and one input, so diagnosis is quick, and because the defect has not yet been built upon by other code or shipped to users; Boehm and Basili found fixes after delivery often cost 100 times more than early fixes.
Unit Testing: What It Is For
5. Who usually writes unit tests, and why? The developer who wrote the unit, because they know its inside best and the tests are needed as soon as the unit exists, long before an independent test team sees the code.
The rest of this subject
These notes are cut from the University's printed syllabus. Open the syllabus itself, or the past papers, for the same subject.