15.- Internal Aspect of JUnit

23-02-2025 - Xavier Salvador

Chapter 15 examines the ComparisonCompactor class from the JUnit framework in depth, demonstrating how to apply Clean Code rules to an already well-written module using the Boy Scout principle: leave the code better than you found it.

What ComparisonCompactor Does

ComparisonCompactor produces readable error messages for equality assertion failures. Given contextLength, expected, and actual, it generates strings such as:

expected: <...B[X]D...>  but was: <...B[Y]D...>

The brackets enclose the differing portion; the ellipses indicate trimmed context. The module had 100% test coverage.

Original Code (Listing 15-2)

The original was correct but improvable code. Private variables used an f prefix (fContextLength, fExpected, fActual, fPrefix, fSuffix), a negative conditional in compact() was not encapsulated, and several names did not clearly convey intent.

Refactoring Steps

1. Remove the f prefix from member variables [N6]

Modern environments make this kind of scope encoding unnecessary:

private int contextLength;
private String expected;
private String actual;
private int prefix;
private int suffix;

2. Encapsulate the negative conditional [G28]

if (shouldNotCompact())
    return Assert.format(message, expected, actual);

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

3. Invert to a positive conditional [G29]

Negative conditions are harder to read. The method is renamed and its logic inverted:

if (canBeCompacted()) { ... }

private boolean canBeCompacted() {
    return expected != null && actual != null && !areStringsEqual();
}

4. Rename compactformatCompactedComparison [N7]

The name compact obscured the side effect of the error check and the return of a formatted message.

5. Extract compactExpectedAndActual() [G30]

A function should do one thing: the body of the if is extracted into a separate method. compactExpected and compactActual are promoted to member variables to maintain return-value consistency [G11].

6. Expose the temporal coupling [G31]

findCommonSuffix depended on findCommonPrefix having been called first. The fix is to merge them into findCommonPrefixAndSuffix(), which calls findCommonPrefix internally before computing the suffix.

7. Rename suffixIndexsuffixLength [N1, G33]

suffixIndex was actually a 1-based length that introduced artificial +1 offsets throughout computeCommonSuffix. Renaming it and adjusting the arithmetic removes those offsets, and compactString can be simplified to:

private String compactString(String source) {
    return computeCommonPrefix()
        + DELTA_START
        + source.substring(prefixLength, source.length() - suffixLength)
        + DELTA_END
        + computeCommonSuffix();
}

Final Version (Listing 15-5)

The result is a class with roughly 10 small methods, each named to describe what it does. The methods startingEllipsis(), startingContext(), delta(), endingContext(), and endingEllipsis() compose the output in a readable way inside compact(String s).

Key Rules

Code Rule applied
N1 Descriptive names
N6 Avoid encodings (f prefix)
N7 Names must describe side effects
G9 Remove dead code and redundant statements
G11 Consistency in conventions
G28 Encapsulate conditionals
G29 Avoid negative conditionals
G30 Functions must do one thing
G31 Expose temporal couplings
G33 Eliminate artificial +1 offsets with proper names

Summary

This chapter demonstrates that even well-written code can be improved. Through small steps — each guided by a specific heuristic — ComparisonCompactor goes from being good to being clean: more expressive, more cohesive, and free of hidden couplings.

results matching ""

    No results matching ""