Skip to main content

Chapter 15: JUnit Internals

A Case Study in Real Codeโ€‹

This chapter examines real code from the JUnit framework โ€” one of the most widely used Java libraries โ€” and applies clean code principles to improve it. It's a valuable reminder that even beloved, battle-tested libraries have room for improvement, and that refactoring is a normal part of professional craftsmanship.


The Subject: ComparisonCompactorโ€‹

Martin examines the ComparisonCompactor class, which is responsible for generating helpful failure messages when a string comparison assertion fails.

For example, when you write:

assertEquals("ABCDE", "ABXDE");

JUnit produces a message like:

expected: <AB[C]DE> but was: <AB[X]DE>

The ComparisonCompactor class generates those [...] difference markers. It's not trivial โ€” it needs to find common prefixes and suffixes and highlight the differing middle section.


The Original Code โ€” Functional but Not Cleanโ€‹

The original code works correctly, but Martin identifies several issues:

Problem 1: Encoded Namesโ€‹

private int fContextLength;
private String fExpected;
private String fActual;
private int fPrefix;
private int fSuffix;

The f prefix is a Hungarian notation relic for "field." In modern Java with IDE support, this encoding is unnecessary noise. Remove the f prefix.

Problem 2: Unencapsulated Conditionalsโ€‹

// Before โ€” conditional logic not extracted
if (fExpected == null || fActual == null || areStringsEqual())
return Assert.format(message, expected, actual);

This condition mixes multiple concerns. Extracting it makes intent clearer:

// After โ€” encapsulated in a named method
if (shouldNotCompact())
return Assert.format(message, expected, actual);

private boolean shouldNotCompact() {
return expected == null || actual == null || areStringsEqual();
}

Problem 3: Confusing Variable Namesโ€‹

The original code uses both expected (the parameter) and fExpected (the field) โ€” similar names for different things within the same scope. This is unnecessarily confusing.

After removing the f prefix from fields, the field and parameter have the same name. Solution: rename the fields to be more descriptive about what they actually are:

// Before
private String fExpected; // the full expected string
// compacted to:
private String expected; // a shortened version used for display

// After โ€” clearer names that distinguish what each variable represents
private String compactExpected;
private String compactActual;

Problem 4: Negated Conditionalsโ€‹

// Negated โ€” requires mental inversion to understand
if (!canBeCompacted()) { ... }

// Better โ€” positive form is easier to read
if (canBeCompacted()) { compact(); }

Problem 5: Hidden Side Effectsโ€‹

The original compact() method returns the comparison message AND sets up internal state (computing prefix/suffix lengths) as a side effect. These are two responsibilities:

// Before โ€” formatCompactedComparison does too much
public String formatCompactedComparison(String message) { ... }

// After โ€” separated clearly
private void compactExpectedAndActual() {
findCommonPrefix();
findCommonSuffix();
compactExpected = compactString(expected);
compactActual = compactString(actual);
}

public String formatCompactedComparison(String message) {
if (canBeCompacted()) {
compactExpectedAndActual();
return Assert.format(message, compactExpected, compactActual);
} else {
return Assert.format(message, expected, actual);
}
}

Problem 6: Hidden Temporal Couplingโ€‹

The original code relied on findCommonSuffix() being called after findCommonPrefix() โ€” because findCommonSuffix used the prefix length computed by findCommonPrefix. But this dependency was invisible:

// Bad โ€” implicit dependency on call order
findCommonPrefix();
findCommonSuffix(); // depends on prefixIndex being set!

Make it explicit:

// Better โ€” the dependency is visible in the signature
private void findCommonSuffix(int prefixIndex) { ... }

// Or better yet โ€” compute both from a shared method
private void findCommonPrefixAndSuffix() {
findCommonPrefix();
// ... then find suffix using prefixIndex
}

The Refactored Versionโ€‹

After applying these improvements, the class is:

  • Easier to read (self-documenting names, no encoding prefixes)
  • Easier to test (concerns are separated)
  • Easier to maintain (hidden dependencies are surfaced)

The logic didn't change. The tests all still pass. But the next developer to work with this code will have a much easier time understanding it.


The Meta-Lessonโ€‹

This chapter demonstrates several important themes:

Even Good Code Can Be Improvedโ€‹

JUnit is among the most well-crafted open-source Java libraries. Yet Martin finds multiple improvements in a small class. This is not a criticism of JUnit's authors โ€” it illustrates that there is always room for improvement, and that improvement is ongoing.

Leave It Better Than You Found Itโ€‹

The improvements Martin makes are small and incremental. None of them are architectural overhauls. Each one is a targeted, safe refactoring backed by a passing test suite. This is the Boy Scout Rule in action.

Test Coverage Enables Confident Refactoringโ€‹

Every refactoring step in this chapter was safe because JUnit has excellent test coverage. Without tests:

  • You can't know if your refactoring changed behavior
  • You won't refactor โ€” fear of regression locks the code in place

The Standard Is Higher Than "It Works"โ€‹

A function that works is not enough. The question is: does it communicate clearly? The original ComparisonCompactor worked perfectly. But it communicated poorly in places โ€” and those were the places Martin improved.


Key Takeawaysโ€‹

  • Remove unnecessary name encodings (f prefix, Hungarian notation) โ€” they add noise without value
  • Extract complex conditionals into named methods that reveal intent
  • Prefer positive conditionals over negated ones
  • Separate concerns: returning a value and setting up state are two different jobs
  • Make temporal coupling (order-dependent operations) explicit in function signatures
  • Working code is the starting point โ€” clean code is the goal
  • Test coverage is what makes safe refactoring possible