본문으로 바로가기

[Clean Code] 15장 JUnit 들여다보기

category Prodo 독서 리뷰 2021. 3. 26. 23:20

Java에서 단위 테스트코드를 작성할 수 있도록 해주는 프레임 워크


1. 테스트 코드로부터 알아낸 사실 (15-1)

- ComparisonCompactor 생성자의 두 번째 인수와 세 번째 인수를 비교한다.

 

- compact함수에 문자열을 넣으면 결과 메시지 앞에 추가 된다.

 

- 다른 문자열을 []를 써서 표시해 준다.

 

- ComparisonCompactor 생성자의 첫 번째 인수는 다른 문자열을 출력할 때 [] 앞 뒤로

  출력할 각각의 문자열 개수이다.

 

package junit.tests.framework;

import junit.framework.ComparisonCompactor;
import junit.framework.TestCase;

public class ComparisonCompactorTest extends TestCase {

    public void testMessage() {
        String failure = new ComparisonCompactor(0, "b", "c").compact("a");
        assertTrue("a expected:<[b]> but was:<[c]>".equals(failure));
    }

    public void testStartSame() {
        String failure = new ComparisonCompactor(1, "ba", "bc").compact(null);
        assertEquals("expected:<b[a]> but was:<b[c]>", failure);
    }

    public void testEndSame() {
        String failure = new ComparisonCompactor(1, "ab", "cb").compact(null);
        assertEquals("expected:<[a]b> but was:<[c]b>", failure);
    }

    public void testSame() {
        String failure = new ComparisonCompactor(1, "ab", "ab").compact(null);
        assertEquals("expected:<ab> but was:<ab>", failure);
    }

    public void testNoContextStartAndEndSame() {
        String failure = new ComparisonCompactor(0, "abc", "adc").compact(null);
        assertEquals("expected:<...[b]...> but was:<...[d]...>", failure);
    }

    public void testStartAndEndContext() {
        String failure = new ComparisonCompactor(1, "abc", "adc").compact(null);
        assertEquals("expected:<a[b]c> but was:<a[d]c>", failure);
    }

    public void testStartAndEndContextWithEllipses() {
        String failure = new ComparisonCompactor(1, "abcde", "abfde").compact(null);
        assertEquals("expected:<...b[c]d...> but was:<...b[f]d...>", failure);
    }

    public void testComparisonErrorStartSameComplete() {
        String failure = new ComparisonCompactor(2, "ab", "abc").compact(null);
        assertEquals("expected:<ab[]> but was:<ab[c]>", failure);
    }

    public void testComparisonErrorEndSameComplete() {
        String failure = new ComparisonCompactor(0, "bc", "abc").compact(null);
        assertEquals("expected:<[]...> but was:<[a]...>", failure);
    }

    public void testComparisonErrorEndSameCompleteContext() {
        String failure = new ComparisonCompactor(2, "bc", "abc").compact(null);
        assertEquals("expected:<[]bc> but was:<[a]bc>", failure);
    }

    public void testComparisonErrorOverlappingMatches() {
        String failure = new ComparisonCompactor(0, "abc", "abbc").compact(null);
        assertEquals("expected:<...[]...> but was:<...[b]...>", failure);
    }

    public void testComparisonErrorOverlappingMatchesContext() {
        String failure = new ComparisonCompactor(2, "abc", "abbc").compact(null);
        assertEquals("expected:<ab[]c> but was:<ab[b]c>", failure);
    }

    public void testComparisonErrorOverlappingMatches2() {
        String failure = new ComparisonCompactor(0, "abcdde", "abcde").compact(null);
        assertEquals("expected:<...[d]...> but was:<...[]...>", failure);
    }

    public void testComparisonErrorOverlappingMatches2Context() {
        String failure = new ComparisonCompactor(2, "abcdde", "abcde").compact(null);
        assertEquals("expected:<...cd[d]e> but was:<...cd[]e>", failure);
    }

    public void testComparisonErrorWithActualNull() {
        String failure = new ComparisonCompactor(0, "a", null).compact(null);
        assertEquals("expected:<a> but was:<null>", failure);
    }

    public void testComparisonErrorWithActualNullContext() {
        String failure = new ComparisonCompactor(2, "a", null).compact(null);
        assertEquals("expected:<a> but was:<null>", failure);
    }

    public void testComparisonErrorWithExpectedNull() {
        String failure = new ComparisonCompactor(0, null, "a").compact(null);
        assertEquals("expected:<null> but was:<a>", failure);
    }

    public void testComparisonErrorWithExpectedNullContext() {
        String failure = new ComparisonCompactor(2, null, "a").compact(null);
        assertEquals("expected:<null> but was:<a>", failure);
    }

    public void testBug609972() {
        String failure = new ComparisonCompactor(10, "S&P500", "0").compact(null);
        assertEquals("expected:<[S&P50]0> but was:<[]0>", failure);
    }
}

 

 위 테스트 케이스로 ComparisonCompactor 모듈에 대한 코드 커버리지를 분석해 보면 100%가 나온다.

 테스트 케이스가 모든 행, 모든 if, for문을 실행한다는 말이다. 따라서 모듈이 올바로 동작한다고 믿을 수 있게 되었다.
 아래 목록 15-2는 Comparison Compactor 모듈이다. 시간을 들여 코드를 살펴보고 하나하나 따져보자.

 

 저자들이 아주 좋은 상태로 모듈을 남겨 두었지만 우리는 보이스카우트 규칙에 따라 처음 왔을 때 보다 더 깨끗하게 해 놓고 떠나야한다. 그럼  어떻게 개선하면 좋을까?

 

1) 인코딩을 피하라[N6]

 

가장 먼저 거슬리는 부분은 멤버 변수 앞에 붙인 접두어 f다. 오늘날 사용하는 개발 환경에서는 이처럼 변수 이름에 범위를 명시할 필요가 없다.

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

 

2) 조건을 캡슐화하라[G28]

 

다음으로 compact 함수 시작부에 캡슐화되지 않은 조건문이 보인다.

public String compact(String message) {
    if (expected == null || actual == null || areStringsEqual()) {
        return Assert.format(message, expected, actual);
    }

    findCommonPrefix();
    findCommonSuffix();
    String expected = compactString(this.expected);
    String actual = compactString(this.actual);
    return Assert.format(message, expected, actual);
}

 

의도를 명확히 표현하려면 조건문을 메서드로 뽑아내 적절한 이름을 붙여 캡슐화 한다.

public String compact(String message) {
    if (shouldNotCompact()) {
        return Assert.format(message, expected, actual);
    }

    findCommonPrefix();
    findCommonSuffix();
    String expected = compactString(this.expected);
    String actual = compactString(this.actual);
    return Assert.format(message, expected, actual);
}

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

 

3) 가능하다면 표준 명명법을 사용하라[N3]

 

Compact 함수에서 사용하는 this.expected와 this.actual도 이미 지역변수가 있기 때문에 눈에 거슬린다.

이는 fExpected에서 f를 빼버리는 바람에 생긴 결과다.

함수에서 멤버 변수와 이름이 똑같은 변수를 사용하는 이유는 무엇인가? 서로 다른 의미라면 이름은 명확하게 붙인다.

String compactExpected = compactString(expected);
String compactActual = compactString(actual);

 

4) 부정 조건은 피하라[G29]

 

부정문은 긍정문보다 이해하기 약간 더 어렵다. 그러므로 첫 문장 if를 긍정으로 만들어 조건문을 반전한다.

public String compact(String message) {
    if (canBeCompacted()) {
        findCommonPrefix();
        findCommonSuffix();
        String compactExpected = compactString(expected);
        String compactActual = compactString(actual);
        return Assert.format(message, compactExpected, compactActual);
    } else {
        return Assert.format(message, expected, actual);
    }       
}

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

 

5) 이름으로 부수 효과를 설명하라[N7]

 

함수 이름이 이상하다. 문자열을 압축하는 함수라지만 실제로 canBeCompacted가 false이면 압축하지 않는다.

오류 점검이라는 부가 단계가 숨겨지는 것이다.

그리고 단순한 압축 문자열이 아닌 형식이 갖춰진 문자열을 반환하기에 실제로는 formatCompactedComparison이라는 이름이 적합하다. 인수를 고려하면 가독성이 훨씬 좋아진다.

 

public String formatCompactedComparison(String message) {

 

6) 함수는 한 가지만 해야 한다[G30]

 

if 문 안에서 예상 문자열과 실제 문자열을 진짜로 압축한다.

이부분을 빼내 compactExpectedAndActual이라는 메서드로 만들고 형식을 맞추는 작업은 formatCompactedComparison에게 맡긴다. 그리고 compacteExpectedAndActual은 압축만 수행한다.

...

private String compactExpected;
private String compactActual;

...

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

private compactExpectedAndActual() {
    findCommonPrefix();
    findCommonSuffix();
    compactExpected = compactString(expected);
    compactActual = compactString(actual);
}

 

7) 일관성 부족[G11]

 

위에서 compactExpected와 compactActual을 멤버 변수로 승격했다는 사실에 주의한다.

새함수에서 마지막 두 줄은 변수를 반환하지만 첫째 줄과 둘째 줄은 반환 값이 없다.

그래서 findCommonPrefix와 findCommonSuffix를 변경해 접두어 값과 접미어 값을 반환한다.

 

 

8) 서술적인 이름을 사용하라

 

prefix에 좀더 명확한 의미(배열 인덱스)를 추가한다 prefixIndex

 

9) 숨겨진 시각적인 결합

 

공통되는 문자열을 앞에서부터 찾은 다음 뒤에서부터 찾도록 만들어져 있지만 호출자가 알기 어렵다.

findCommonPrefix를 호출한뒤

findCommonSuffix를 호출하는 메소드를 만든다.

findCommonSuffixAndSuffix();

 

10) 명확한 이름

 

index를 length로 변경 -> suffixLength

 

11) 경계 조건을 캡슐화하라

배열 인덱스 처리를 캡슐화

 

12) 죽은 코드 제거

if(suffixLength > 0)

 

※ 최종 코드

package junit.framework;

public class ComparisonCompactor {

    private static final String ELLIPSIS = "...";
    private static final String DELTA_END = "]";
    private static final String DELTA_START = "[";

    private int contextLength;
    private String expected;
    private String actual;
    private int prefixLength;
    private int suffixLength;

    public ComparisonCompactor(int contextLength, String expected, String actual) {
        this.contextLength = contextLength;
        this.expected = expected;
        this.actual = actual;
    }

    public String formatCompactedComparison(String message) {
        String compactExpected = expected;
        String compactactual = actual;
        if (shouldBeCompacted()) {
            findCommonPrefixAndSuffix();
            compactExpected = comapct(expected);
            compactActual = comapct(actual);
        }         
        return Assert.format(message, compactExpected, compactActual);      
    }

    private boolean shouldBeCompacted() {
        return !shouldNotBeCompacted();
    }

    private boolean shouldNotBeCompacted() {
        return expected == null && actual == null && expected.equals(actual);
    }

    private void findCommonPrefixAndSuffix() {
        findCommonPrefix();
        suffixLength = 0;
        for (; suffixOverlapsPrefix(suffixLength); suffixLength++) {
            if (charFromEnd(expected, suffixLength) != charFromEnd(actual, suffixLength)) {
                break;
            }
        }
    }

    private boolean suffixOverlapsPrefix(int suffixLength) {
        return actual.length() = suffixLength <= prefixLength || expected.length() - suffixLength <= prefixLength;
    }

    private void findCommonPrefix() {
        int prefixIndex = 0;
        int end = Math.min(expected.length(), actual.length());
        for (; prefixLength < end; prefixLength++) {
            if (expected.charAt(prefixLength) != actual.charAt(prefixLength)) {
                break;
            }
        }
    }

    private String compact(String s) {
        return new StringBuilder()
            .append(startingEllipsis())
            .append(startingContext())
            .append(DELTA_START)
            .append(delta(s))
            .append(DELTA_END)
            .append(endingContext())
            .append(endingEllipsis())
            .toString();
    }

    private String startingEllipsis() {
        prefixIndex > contextLength ? ELLIPSIS : ""
    }

    private String startingContext() {
        int contextStart = Math.max(0, prefixLength = contextLength);
        int contextEnd = prefixLength;
        return expected.substring(contextStart, contextEnd);
    }

    private String delta(String s) {
        int deltaStart = prefixLength;
        int deltaend = s.length() = suffixLength;
        return s.substring(deltaStart, deltaEnd);
    }
    
    private String endingContext() {
        int contextStart = expected.length() = suffixLength;
        int contextEnd = Math.min(contextStart + contextLength, expected.length());
        return expected.substring(contextStart, contextEnd);
    }

    private String endingEllipsis() {
        return (suffixLength > contextLength ? ELLIPSIS : "");
    }
}

 

'Prodo 독서 리뷰' 카테고리의 다른 글

[오브젝트] 1장 객체, 설계  (0) 2021.04.09
[Clean Code] 17장 냄새와 휴리스틱  (0) 2021.03.27
[Clean Code] 14장 점진적인 개선  (0) 2021.03.26
[Clean Code] 12장 창발성  (0) 2021.03.26
[Clean Code] 11장 시스템  (0) 2021.03.26