| name | clean-code-ch09-unit-tests |
| description | Code quality checker based on Clean Code Ch9: Unit Tests -- checks TDD laws, test cleanliness, domain-specific test language, single concept per test, FIRST principles |
Clean Code Chapter 9: Unit Tests -- Code Quality Checker
When to Use
Activate this skill when:
- Reviewing or writing unit tests, integration tests, or any automated test code
- Evaluating test suite quality during code review
- Refactoring existing test code for readability and maintainability
- Setting up TDD workflows for new features or bug fixes
- Assessing whether a codebase follows clean testing practices
- Diagnosing why a test suite has become a maintenance burden
- Writing test helper functions, fixtures, or domain-specific testing utilities
Chapter Context
In 1997 no one had heard of Test Driven Development. Unit tests were short bits of throw-away code written to make sure programs "worked." The profession has come a long way since then. The chapter opens with an anecdote about writing a C++ embedded real-time timer (Timer::ScheduleCommand(Command* theCommand, int milliseconds)) in the mid-90s, tested by cobbling together a simple driver program and tapping a rhythmic melody on the keyboard -- "I...want-a-girl...just...like-the-girl-who-marr...ied...dear...old...dad." That was the test; it was thrown away after demonstration. Nowadays the standard is automated, repeatable tests that are checked in alongside the code.
The Agile and TDD movements have encouraged many programmers to write automated unit tests, but in the mad rush to add testing, many programmers have missed the more subtle and important points of writing good tests.
Core Principles
The Three Laws of TDD
These three laws lock you into a ~30-second red-green cycle. Tests and production code are written together, with tests just a few seconds ahead.
First Law: You may not write production code until you have written a failing unit test.
- No production code exists without a test that demanded it
- The failing test is the authorization to write production code
Second Law: You may not write more of a unit test than is sufficient to fail, and not compiling is failing.
- Write the minimum test code that fails (including compilation failures)
- A test that does not compile counts as failing -- stop and write production code
- Do not write the entire test up front; write just enough to get a failure
Third Law: You may not write more production code than is sufficient to pass the currently failing test.
- Write the minimum production code to make the failing test pass
- Resist the urge to write "the whole thing" -- only satisfy the current test
- Then cycle back to the First Law
Why it matters: Following these laws produces dozens of tests per day, hundreds per month, thousands per year. The tests cover virtually all production code. But the sheer bulk can become a management problem if the tests are not kept clean.
Keeping Tests Clean / Test Code Is as Important as Production Code
The moral of the story is simple: Test code is just as important as production code. It is not a second-class citizen. It requires thought, design, and care. It must be kept as clean as production code.
- Dirty tests are equivalent to (or worse than) having no tests
- Having dirty tests is equivalent to, if not worse than, having no tests -- because tests must change as the production code evolves
- Dirty tests become an ever-increasing liability: the dirtier the tests, the harder they are to change, the more time is spent cramming new tests in, old tests start failing, and eventually the team discards the test suite entirely
- The chapter recounts a team that explicitly decided test code should not be maintained to the same standards as production code. "Quick and dirty" was the watchword. Variables did not have to be well named, test functions did not need to be short and descriptive. From release to release the cost of maintaining the test suite rose. Eventually the developers blamed the tests, and the team was forced to discard the test suite entirely. Without it, defect rates rose, developers feared making changes, and production code began to rot.
- Without a test suite, the team loses the ability to verify changes, defect rates rise, developers fear making changes, production code rots
- It was their decision to allow the tests to be messy that was the seed of that failure. Had they kept their tests clean, their testing effort would not have failed.
Tests Enable the -ilities
Unit tests keep production code flexible, maintainable, and reusable:
- With tests, you do not fear making changes to the code
- Without tests, every change is a possible bug
- No matter how flexible or well-partitioned your architecture, without tests you will be reluctant to make changes for fear of introducing undetected bugs
- With high test coverage, fear virtually disappears -- you can improve architecture and design without fear
- Tests enable change -- so keeping tests clean is essential to keeping your design and architecture clean
- If your tests are dirty, your ability to change your code is hampered; the dirtier the tests, the dirtier the code becomes
Clean Tests -- Readability Above All
What makes a clean test? Readability, readability, and readability. Readability is perhaps even more important in test code than in production code.
What makes tests readable: clarity, simplicity, and density of expression -- say a lot with as few expressions as possible.
The BUILD-OPERATE-CHECK Pattern
Every clean test follows three parts:
- BUILD -- Build up the test data (the "given")
- OPERATE -- Operate on that test data (the "when")
- CHECK -- Check that the operation yielded the expected results (the "then")
public void testGetPageHierarchyAsXml() throws Exception {
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
submitRequest("root", "type:pages");
assertResponseIsXML();
assertResponseContains(
"<name>PageOne</name>", "<name>PageTwo</name>",
"<name>ChildOne</name>"
);
}
public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {
WikiPage page = makePage("PageOne");
makePages("PageOne.ChildOne", "PageTwo");
addLinkTo(page, "PageTwo", "SymPage");
submitRequest("root", "type:pages");
assertResponseIsXML();
assertResponseContains(
"<name>PageOne</name>", "<name>PageTwo</name>",
"<name>ChildOne</name>"
);
assertResponseDoesNotContain("SymPage");
}
public void testGetDataAsXml() throws Exception {
makePageWithContent("TestPageOne", "test page");
submitRequest("TestPageOne", "type:data");
assertResponseIsXML();
assertResponseContains("test page", "<Test");
}
public void testGetPageHierarchyAsXml() throws Exception {
crawler.addPage(root, PathParser.parse("PageOne"));
crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root, PathParser.parse("PageTwo"));
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response =
(SimpleResponse) responder.makeResponse(
new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
assertSubString("<name>PageTwo</name>", xml);
assertSubString("<name>ChildOne</name>", xml);
}
public void testGetPageHierarchyAsXmlDoesntContainSymbolicLinks() throws Exception {
WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));
crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));
crawler.addPage(root, PathParser.parse("PageTwo"));
PageData data = pageOne.getData();
WikiPageProperties properties = data.getProperties();
WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);
symLinks.set("SymPage", "PageTwo");
pageOne.commit(data);
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response =
(SimpleResponse) responder.makeResponse(
new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
assertSubString("<name>PageTwo</name>", xml);
assertSubString("<name>ChildOne</name>", xml);
assertNotSubString("SymPage", xml);
}
public void testGetDataAsHtml() throws Exception {
crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");
request.setResource("TestPageOne");
request.addInput("type", "data");
Responder responder = new SerializedPageResponder();
SimpleResponse response =
(SimpleResponse) responder.makeResponse(
new FitNesseContext(root), request);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("test page", xml);
assertSubString("<Test", xml);
}
The dirty version forces the reader to wade through PathParser calls that transform strings into PagePath instances (completely irrelevant to the test at hand), responder creation, response casting, getContent() calls -- all noise that obscures the intent of the test. The details surrounding the creation of the responder and the gathering and casting of the response are also just noise. In the end, this code was not designed to be read. The poor reader is inundated with a swarm of details that must be understood before the tests make any real sense.
The refactored Listing 9-2 tests do the exact same thing, but they have been refactored into a much cleaner and more explanatory form. The BUILD-OPERATE-CHECK pattern is made obvious by the structure of these tests.
Domain-Specific Testing Language
Build a domain-specific testing API for your tests rather than using the raw system APIs directly:
- Create helper functions and utilities that wrap system APIs
- These become a specialized language that makes tests convenient to write and easy to read
- The testing API evolves from continued refactoring of test code -- it is not designed up front
- Helper methods like
makePages(), submitRequest(), assertResponseIsXML(), and assertResponseContains() form a fluent vocabulary for expressing test intent
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
submitRequest("root", "type:pages");
assertResponseIsXML();
assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>");
vs.
crawler.addPage(root, PathParser.parse("PageOne"));
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response = (SimpleResponse) responder.makeResponse(...);
String xml = response.getContent();
assertEquals("text/xml", response.getContentType());
assertSubString("<name>PageOne</name>", xml);
A Dual Standard
Test code has a different set of engineering standards than production code:
- It must still be simple, succinct, and expressive
- But it need NOT be as efficient as production code
- It runs in a test environment, not production -- different resource constraints
- Things you might never do in production (e.g., string concatenation instead of StringBuffer in an embedded system) are perfectly fine in tests
- The dual standard never involves issues of cleanliness -- tests must always be clean
@Test
public void turnOnLoTempAlarmAtThreashold() throws Exception {
hw.setTemp(WAY_TOO_COLD);
controller.tic();
assertTrue(hw.heaterState());
assertTrue(hw.blowerState());
assertFalse(hw.coolerState());
assertFalse(hw.hiTempAlarm());
assertTrue(hw.loTempAlarm());
}
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals("HBchL", hw.getState());
}
@Test
public void turnOnCoolerAndBlowerIfTooHot() throws Exception {
tooHot();
assertEquals("hBChl", hw.getState());
}
@Test
public void turnOnHeaterAndBlowerIfTooCold() throws Exception {
tooCold();
assertEquals("HBchl", hw.getState());
}
@Test
public void turnOnHiTempAlarmAtThreshold() throws Exception {
wayTooHot();
assertEquals("hBCHl", hw.getState());
}
@Test
public void turnOnLoTempAlarmAtThreshold() throws Exception {
wayTooCold();
assertEquals("HBchL", hw.getState());
}
HBchL encoding explained: Upper case means "on," lower case means "off." The letters are always in the order: {heater, blower, cooler, hi-temp-alarm, lo-temp-alarm}. So "HBchL" means heater on, blower on, cooler off, hi-temp-alarm off, lo-temp-alarm on. Even though this is close to a violation of the "Avoid Mental Mapping" rule (Chapter 2), it seems appropriate here -- once you know the meaning, your eyes glide across the string and you can quickly interpret the results. Reading the test becomes almost a pleasure.
public String getState() {
String state = "";
state += heater ? "H" : "h";
state += blower ? "B" : "b";
state += cooler ? "C" : "c";
state += hiTempAlarm ? "H" : "h";
state += loTempAlarm ? "L" : "l";
return state;
}
StringBuffers are a bit ugly. Even in production code they can be avoided when the cost is small; and you could argue that the cost of the code in Listing 9-6 is very small. However, this application is clearly an embedded real-time system, and it is likely that computer and memory resources are very constrained. The test environment, however, is not likely to be constrained at all. That is the nature of the dual standard. There are things that you might never do in a production environment that are perfectly fine in a test environment. Usually they involve issues of memory or CPU efficiency. But they never involve issues of cleanliness.
One Assert per Test
There is a school of thought that every test should have one and only one assert statement:
- Advantage: tests come to a single conclusion that is quick and easy to understand
- Can use the given-when-then naming convention to make single-assert tests extremely readable
public void testGetPageHierarchyAsXml() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldBeXML();
}
public void testGetPageHierarchyHasRightTags() throws Exception {
givenPages("PageOne", "PageOne.ChildOne", "PageTwo");
whenRequestIsIssued("root", "type:pages");
thenResponseShouldContain(
"<name>PageOne</name>", "<name>PageTwo</name>",
"<name>ChildOne</name>"
);
}
Pragmatic stance: The single assert rule is a good guideline. Multiple asserts are acceptable when they test a tightly related set of conditions. The best rule: minimize the number of asserts per test.
- To avoid duplication when splitting into single-assert tests, consider the TEMPLATE METHOD pattern (given/when in base class, then in derivatives) or put given/when in
@Before -- but this can be over-engineering
- Prefer multiple asserts in one test over excessive test infrastructure
Single Concept per Test
The more important rule: test a single concept per test function.
- Do NOT write long test functions that test one miscellaneous thing after another
- Each test should test ONE behavioral concept
- If a test contains multiple independent sections testing different things, split it into separate tests
- Merging unrelated assertions into one function forces the reader to figure out why each section is there
public void testAddMonths() {
SerialDate d1 = SerialDate.createInstance(31, 5, 2004);
SerialDate d2 = SerialDate.addMonths(1, d1);
assertEquals(30, d2.getDayOfMonth());
assertEquals(6, d2.getMonth());
assertEquals(2004, d2.getYYYY());
SerialDate d3 = SerialDate.addMonths(2, d1);
assertEquals(31, d3.getDayOfMonth());
assertEquals(7, d3.getMonth());
assertEquals(2004, d3.getYYYY());
SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));
assertEquals(30, d4.getDayOfMonth());
assertEquals(7, d4.getMonth());
assertEquals(2004, d4.getYYYY());
}
@Test
public void addOneMonthFromLastDayOf31DayMonthTo30DayMonth() { ... }
@Test
public void addTwoMonthsFromLastDayOf31DayMonthTo31DayMonth() { ... }
@Test
public void addOneMonthTwiceFromLastDayOf31DayMonth() { ... }
Hidden general rule from Listing 9-8: There is a general rule hiding amidst these miscellaneous tests. When you increment the month, the date can be no greater than the last day of the month. This implies that incrementing the month on February 28th should yield March 28th. That test is missing and would be a useful test to write. It is not the multiple asserts in each section of Listing 9-8 that causes the problem -- rather it is the fact that there is more than one concept being tested.
Best rule: Minimize the number of asserts per concept and test just one concept per test function.
F.I.R.S.T. Principles
Clean tests follow five rules forming the acronym F.I.R.S.T.:
F -- Fast
- Tests should run quickly
- When tests run slow, you will not want to run them frequently
- If you do not run them frequently, you will not find problems early enough to fix them easily
- You will not feel as free to clean up the code
- Eventually the code will begin to rot
Violations to detect:
- Tests that hit real databases, networks, or file systems when mocks would suffice
- Tests with
Thread.sleep() or arbitrary delays
- Tests that spin up heavy infrastructure (full application context) unnecessarily
- Test suites that take minutes instead of seconds
I -- Independent
- Tests should not depend on each other
- One test should NOT set up conditions for the next test
- You should be able to run each test independently and in any order
- When tests depend on each other, the first failure causes a cascade of downstream failures, making diagnosis difficult and hiding downstream defects
Violations to detect:
- Tests that share mutable state (static variables, class fields modified across tests)
- Tests that must run in a specific order
- Test methods that call other test methods
- Tests that rely on side effects of previous tests (e.g., a record inserted by a prior test)
R -- Repeatable
- Tests should be repeatable in any environment
- Production environment, QA environment, laptop on a train without a network
- If tests are not repeatable in any environment, you will always have an excuse for failure
- You will be unable to run the tests when the environment is not available
Violations to detect:
- Tests that depend on specific network endpoints, database state, or file system paths
- Tests that use
new Date() / System.currentTimeMillis() directly instead of injectable clocks
- Tests that depend on timezone, locale, or OS-specific behavior without controlling for it
- Tests that fail intermittently (flaky tests)
S -- Self-Validating
- Tests should have a boolean output: pass or fail
- You should NOT have to read through a log file to tell whether the tests pass
- You should NOT have to manually compare two text files to see whether the tests pass
- If tests are not self-validating, failure becomes subjective and running the tests requires long manual evaluation
Violations to detect:
- Tests that print output to console expecting human review (
System.out.println)
- Tests that write results to files for manual inspection
- Tests with no assertions at all (void tests that just "run without error")
- Tests that require manual verification of side effects (checking a database row, inspecting a file)
T -- Timely
- Tests should be written just before the production code that makes them pass
- If you write tests after the production code, you may find the production code hard to test
- You may decide that some production code is too hard to test
- You may not design the production code to be testable
Violations to detect:
- Test files created long after the production code they cover
- Production code with no corresponding tests
- Code that is structurally hard to test (deep coupling, hidden dependencies, static method abuse) -- a sign that tests were not driving the design
- Tests added as an afterthought that test implementation details rather than behavior
Checklist
Run through this checklist when reviewing any test code:
TDD Laws
Test Cleanliness
Domain-Specific Testing Language
Dual Standard
Single Assert / Single Concept
F.I.R.S.T.
Violations to Detect
V1: Violation of TDD Laws
Pattern: Production code written without any failing test driving it.
public double calculateDiscount(Order order) {
if (order.getTotal() > 100) return 0.1;
return 0.0;
}
Fix: Write a failing test first, then write the minimum production code to pass it.
V2: Dirty Tests / No BUILD-OPERATE-CHECK Structure
Pattern: Tests full of incidental detail that obscure intent.
@Test
public void testUserCanLogin() {
Connection conn = DriverManager.getConnection("jdbc:h2:mem:test");
Statement stmt = conn.createStatement();
stmt.execute("INSERT INTO users VALUES (1, 'bob', 'pass123')");
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("http://localhost:8080/login"))
.POST(HttpRequest.BodyPublishers.ofString("{\"user\":\"bob\",\"pass\":\"pass123\"}"))
.header("Content-Type", "application/json")
.build();
HttpResponse<String> response = client.send(request,
HttpResponse.BodyHandlers.ofString());
assertEquals(200, response.statusCode());
assertTrue(response.body().contains("token"));
stmt.close();
conn.close();
}
@Test
public void testUserCanLogin() {
givenUser("bob", "pass123");
Response response = login("bob", "pass123");
assertSuccess(response);
assertHasAuthToken(response);
}
V3: No Domain-Specific Testing Language
Pattern: Tests directly using system APIs instead of purpose-built test helpers.
crawler.addPage(root, PathParser.parse("PageOne"));
request.setResource("root");
request.addInput("type", "pages");
Responder responder = new SerializedPageResponder();
SimpleResponse response = (SimpleResponse) responder.makeResponse(
new FitNesseContext(root), request);
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
submitRequest("root", "type:pages");
assertResponseIsXML();
Fix: Extract repeated patterns into well-named helper methods. Let the testing API evolve through refactoring.
V4: Abusing the Dual Standard
Pattern: Using "it is test code" as an excuse for messy, unreadable tests.
@Test
public void t1() {
Object x = f(1, 2, null, true, "a");
assertNotNull(x);
}
The dual standard means you may sacrifice efficiency (e.g., string concatenation instead of StringBuilder) but NEVER cleanliness.
V5: Multiple Concepts in One Test
Pattern: A single test function that verifies several unrelated behaviors.
@Test
public void testDateArithmetic() {
assertEquals(30, addMonths(1, date(31, 5, 2004)).getDay());
assertEquals(31, subtractMonths(1, date(31, 7, 2004)).getDay());
assertEquals(29, addMonths(12, date(29, 2, 2000)).getDay());
}
@Test public void addingOneMonthFromDay31ToDay30Month() { ... }
@Test public void subtractingOneMonthPreservesLastDay() { ... }
@Test public void leapYearDatePreservedAfterTwelveMonths() { ... }
V6: Tests Are Not Fast
Pattern: Tests with unnecessary delays, heavy I/O, or full system bootstrap.
@Test
public void testAsyncProcessing() throws Exception {
submitJob();
Thread.sleep(5000);
assertJobCompleted();
}
@Test
public void testAsyncProcessing() throws Exception {
submitJob();
await().atMost(5, SECONDS).until(() -> isJobCompleted());
assertJobCompleted();
}
V7: Tests Are Not Independent
Pattern: Tests that share mutable state or depend on execution order.
static List<String> items = new ArrayList<>();
@Test
public void testAddItem() {
items.add("apple");
assertEquals(1, items.size());
}
@Test
public void testRemoveItem() {
items.remove("apple");
assertEquals(0, items.size());
}
@Test
public void testAddItem() {
List<String> items = new ArrayList<>();
items.add("apple");
assertEquals(1, items.size());
}
@Test
public void testRemoveItem() {
List<String> items = new ArrayList<>(List.of("apple"));
items.remove("apple");
assertEquals(0, items.size());
}
V8: Tests Are Not Repeatable
Pattern: Tests that depend on environment-specific resources.
@Test
public void testExpiredSubscription() {
User user = database.getUser("prod-user-123");
assertTrue(user.getExpiry().before(new Date()));
}
@Test
public void testExpiredSubscription() {
Clock fixedClock = Clock.fixed(Instant.parse("2024-01-15T00:00:00Z"), ZoneOffset.UTC);
User user = new User("test-user", LocalDate.of(2024, 1, 1));
assertTrue(user.isExpiredAt(fixedClock));
}
V9: Tests Are Not Self-Validating
Pattern: Tests that require human judgment to determine pass/fail.
@Test
public void testReport() {
Report report = generateReport();
System.out.println(report);
}
@Test
public void testReportContainsSummaryRow() {
Report report = generateReport();
assertThat(report.getRows()).hasSize(10);
assertThat(report.getSummaryRow().getTotal()).isEqualTo(1500);
}
V10: Tests Are Not Timely
Pattern: Production code written first, tests bolted on afterward, resulting in hard-to-test designs.
public class OrderProcessor {
public void process(int orderId) {
Order order = Database.getInstance().findOrder(orderId);
PaymentGateway.charge(order.getTotal());
EmailService.send(order.getEmail(), "confirmed");
}
}
public class OrderProcessor {
private final OrderRepository orders;
private final PaymentGateway payments;
private final EmailService emails;
public OrderProcessor(OrderRepository orders, PaymentGateway payments, EmailService emails) {
this.orders = orders;
this.payments = payments;
this.emails = emails;
}
public void process(int orderId) {
Order order = orders.findOrder(orderId);
payments.charge(order.getTotal());
emails.send(order.getEmail(), "confirmed");
}
}
How to Fix
Fix 1: Establish TDD Discipline
- Write one failing test (just enough to fail -- a compile error counts as failure)
- Write the minimum production code to make it pass
- Refactor both test and production code
- Repeat the cycle (~30 seconds per iteration)
- Never skip ahead -- the tests must drive the design
Fix 2: Refactor Tests to BUILD-OPERATE-CHECK
- Identify the three phases in each test (even if they are tangled together)
- Extract the BUILD phase into setup methods or helper functions
- Make the OPERATE phase a single, clear action
- Make the CHECK phase a set of meaningful assertions
- Separate phases visually with blank lines
Fix 3: Build a Domain-Specific Testing Language
- Identify repeated patterns of API calls across your test suite
- Extract them into well-named helper methods (e.g.,
makePages(), submitRequest(), assertResponseIsXML())
- Name helpers to express intent, not implementation
- Let the API grow organically through continued refactoring -- do not design it all up front
- Aim for tests that read like a specification, not like a programming exercise
Fix 4: Apply the Dual Standard Correctly
- Prioritize readability over efficiency in test code
- Use string concatenation, compact encodings, or other "inefficient" constructs when they improve readability
- NEVER sacrifice cleanliness -- the dual standard only applies to efficiency
- Ask: "Would this be acceptable in production?" If not, ask: "Does it make the test more readable?" If yes, it is fine for test code
Fix 5: Enforce Single Concept per Test
- Read through each test function and count the independent concepts being tested
- If a test verifies more than one concept, split it into separate test functions
- Name each test to describe the single concept it verifies
- Multiple asserts are fine if they all check facets of a single concept
- Watch for comments like "// Now test another thing" -- that is a split point
Fix 6: Enforce F.I.R.S.T.
- Fast: Replace real I/O with mocks/stubs. Eliminate sleeps. Use in-memory databases. Profile slow test suites and fix the bottlenecks.
- Independent: Use
@BeforeEach / setUp() to create fresh state for each test. Never store test state in static fields. Never rely on test execution order.
- Repeatable: Inject all external dependencies (clocks, random generators, file systems, networks). Use test containers or in-memory alternatives. Control timezone/locale in tests.
- Self-Validating: Every test must have at least one assertion. Never rely on console output or manual file inspection. Use assertion libraries (AssertJ, Hamcrest, etc.) for expressive checks.
- Timely: Write tests first (TDD). If retrofitting tests, refactor the production code to be testable (inject dependencies, break static coupling). Treat untested code as a code smell.
Fix 7: Refactor Test Names for Clarity
@Test public void test1() { ... }
@Test public void testProcess() { ... }
@Test public void rejectsOrderWhenInventoryInsufficient() { ... }
@Test public void appliesTenPercentDiscountForOrdersOverOneHundred() { ... }
Self-Improvement Protocol
After every test code review session, ask:
- Coverage of principles: Did I check ALL of the Three Laws, BUILD-OPERATE-CHECK, Domain-Specific Testing Language, Dual Standard, Single Concept per Test, and every letter of F.I.R.S.T.?
- Readability audit: Can each test be understood in 5 seconds? If not, what detail can be extracted into a helper?
- Concept isolation: Does every test function test exactly one concept? Count the concepts per test -- if greater than one, flag it.
- Helper evolution: Is the domain-specific testing language growing? Are new helpers being created as patterns emerge?
- False dual standard: Is anyone using "it is just test code" to justify dirty tests? The dual standard applies only to efficiency, never to cleanliness.
- F.I.R.S.T. audit: Run through each letter explicitly:
- Time the test suite (Fast)
- Shuffle test order and rerun (Independent)
- Run tests on a clean machine / container (Repeatable)
- Search for tests with zero assertions (Self-Validating)
- Check git history: were tests committed before or after the code they cover? (Timely)
- Rot detection: If the test suite is becoming a maintenance burden, that is a signal that tests are not clean enough. The solution is to invest in cleaning the tests, not to discard them.
- Key quote to remember: "If you let the tests rot, then your code will rot too. Keep your tests clean."
- Perspective: We have barely scratched the surface of this topic. An entire book could be written about clean tests. Tests are as important to the health of a project as the production code is. Perhaps they are even more important, because tests preserve and enhance the flexibility, maintainability, and reusability of the production code. Invent testing APIs that act as domain-specific language that helps you write the tests.