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