JUnit Internals
JUnit's ComparisonCompactor is the small utility that produces the familiar
expected:<...B[X]D...> but was:<...B[Y]D...> message when a string assertion fails. Given
two strings that differ, it finds the parts they have in common and collapses everything else
into a compact diff. It is a good refactoring case study for a specific reason: the starting
code is not bad. Kent Beck and Erich Gamma wrote it, it is fully covered by tests, and it
works. The exercise is not "fix broken code," it is "make already-good code better," which is
closer to the refactoring you actually do day to day than cleaning up a disaster.
Reading the tests before the code
The chapter's first move is to read the unit tests before reading the implementation. A
well-named test suite documents the contract of a class better than prose does, and for
ComparisonCompactor the tests spell out exactly what "compact" means for every edge case:
matching prefixes, matching suffixes, no context, overlapping matches, null inputs. If you
ever inherit a class with no documentation, the tests are the documentation. Reading them
first means you refactor knowing what must keep working.
What was actually wrong with it
Nothing dramatic, which is the point. The problems are all small frictions that compound:
- Scope-encoded names. Every member variable is prefixed with
f(fExpected,fPrefix,fContextLength), a convention from an era before IDEs could tell you at a glance whether a variable was a field or a local. The prefix adds nothing today and forces a small mental translation on every read. - An unencapsulated, negative conditional. The
compactmethod opens withif (fExpected == null || fActual == null || areStringsEqual()), a three-part condition inline, phrased as a negative ("don't compact when..."). A reader has to parse the whole expression before understanding what the branch is for. - A misleading method name. The method is called
compact, but when the early-exit condition is true it does not compact anything - it just formats the raw strings. The name promises a narrower behavior than the method delivers. - Magic off-by-ones.
fSuffixis calculated as a one-based length, which is whycomputeCommonSuffixis full of+1s scattered through the arithmetic. The moment you rebasesuffixLengthto be genuinely zero-based, the+1s disappear and twoifstatements that looked load-bearing turn out to be dead weight (comment them out, tests still pass, delete them).
The moves that fixed it
The representative sequence, condensed into the comparison above: drop the f prefix,
extract the inline conditional into a named predicate (shouldNotBeCompacted), invert it to
read positively (shouldBeCompacted), then split the method in two. One method
(formatCompactedComparison) does the formatting and owns the early-exit branch; a second,
smaller method does nothing but the actual compacting. Renaming a variable to fix an
off-by-one, then noticing that the fix makes a conditional unreachable, is refactoring at its
most typical: one small, safe change exposes the next one.
One extraction didn't survive contact with reality. Computing the prefix and the suffix
started out as two independent-looking methods, but finding the suffix secretly depended on
the prefix having been found first - a hidden temporal coupling. Passing the prefix in as an
argument made the ordering work, but the argument itself looked arbitrary to anyone reading
the signature cold, so the two methods were merged into one findCommonPrefixAndSuffix
instead, which makes the dependency impossible to get wrong. Martin is upfront that the final
version also reverses a few earlier micro-decisions - some extracted methods get inlined back
in, and a conditional's sense flips a second time - because refactoring converges through
trial and error, not a straight line from bad to good.
The transferable lesson
None of these changes are individually impressive. That is exactly why the chapter is worth
reading: it demonstrates that "clean code" is mostly the accumulation of many small,
low-risk renames and extractions, each one made possible by the safety net of a passing test
suite, rather than one clever rewrite. Even code written by the framework's own authors, with
full test coverage, has room for a few more honest names and one fewer unencapsulated if.