본문 바로가기

카테고리 없음

Junit 분석

728x90

 JUnit은 저자가 많다 하지만 시작은 켄트벡과 에릭 감마, 두 사람이다. 두 사람이 함께 아틀란타 행 비행기를 타고 가다 JUnit을 만들었다. 켄트는 자바를 배우고 싶었고 에릭은 켄트의 스몰토크 테스트 프레임워크를 배우고 싶었다. "비좁은 기내에서 엔지니어 둘이 랩탑을 꺼내 코드를 짜는 일 밖에는 다른 무엇을 하겠는가?" 공중에서 세 시간 정도 일한 끝에 두 사람은 JUnit 기초를 구현했다.

 

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);
    }
}

코드 커버리지 100%  달성 (12장 모든 테스트를 작성하라 -> 리펙토링 과정을 거칠수 있다.)

 

 

1. 접두어 제거

 

접두어는 불필요한 중복 정보

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

2. 캡슐화 되지 않은 조건문, 애매모호한 변수명명

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 compactExpected = compactString(this.expected); // 애매모호하게 중복된 변수명
    String compactActual = compactString(this.actual); //애매모호하게 중복된 변수명
    return Assert.format(message, expected, actual);
}

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

3) 부정문보다는 긍정문

 

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();
}

4) 함수 이름으로 부수 효과를 설명하라.

canBeCompacted가 false이면 압축하지 않는다. compact라는 함수에 이 조건문 단계가 숨겨져있다.

또한 압축된 문자열이 아니라 형식이 갖춰진 문자열을 반환한다. 그러므로 public String formatCompactedComparison(String message) 로 함수명을 바꾼다.

 

5) 함수 분리

 

if문안에서 예상 문자열과 실제 문자열을 진짜로 압축한다. 이  부분을 빼내서 따로 메서드를 만든다. 

또한 형식을 맞추는 작업은 다른 함수에 맡긴다.

...

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() { //실제 압축은 여기서 한다.
    prefixIndex = findCommonPrefix();
    suffixIndex = findCommonSuffix();
    compactExpected = compactString(expected);
    compactActual = compactString(actual);
}

 

compactExpectedAndActual에서 첫 두 줄은 반환값이 없다. 마지막 두줄은 변수를 반환한다.  함수의 일관성이 부족하다.

이를 수정한다.

 

시간적인 결합(findCommonSuffix는 findCommonPrefix 계산에 의존한다. 그러므로 호출 순서가 중요하다.)

시간 결합을 외부에 노출하고자 인수로 나타낸다. 연결소자를 생성해 시간적인 결합 순서를 노출한다.

(각 함수가 내놓은 결과가 다음 함수에 필요한 구조로 순서를 바꿔 호출할수가 없다.)

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

6) 일관성 을 유지하라

private void findCommonPrefixAndSuffix() {
    findCommonPrefix();
    int expectedSuffix = expected.length() - 1;
    int actualSuffix = actual.length() - 1;
    for (; actualSuffix >= prefixIndex && expectedSuffix >= prefix; actualSuffix--, expectedSuffix--) {
        if (expected.charAt(expectedSuffix) != actual.charAt(actualSuffix)) {
            break;
        }
    }
    suffixIndex = expected.length() - expectedSuffix;
}

인수로 전달하는 방식으로 함수 호출 순서는 확실히 정해지지만 왜 prefixIndex가 필요한지 설명하지 못한다.

원래대로 되돌리고 함수이름을 findCommonPrefixAndSuffix로 바꾸고 이 함수내에서 findCommonPrefix를 먼저 호출한다.

 

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

 suffixIndex가 실제로는 접미어 길이라는 사실이 드러난다. prefixIndex도 마찬가지로 이 경우 "index"와 "length"가 동의어다. 비록 그렇다고 하더라구 "length"가 더 합당하다. 실제로 suffixIndex는 0이 아닌 1에서 시작하므로 진정한 길이가 아니다. computeCommSuffix에 +1 하는 것이 곳곳에 등장하는 이유도 여기 있다.

 

8) 죽은 코드 제거

suffixLength가 1씩 감소되었으므로 모든 > 연산자를 >= 연산자로 바꾼다.

if(suffixLength > 0 ) 도 if(suffixLength >=0) 으로 바꿔야하나 

원래 코드에서 suffixIndex 은 항상 1보다 컸으므로 if문은 있으나마나다.

그러므로 불필요한 if문을 모두 걷어낸다.

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

최종 코드

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 : "");
    }
}

 

- 모듈은 분석함수와 조합함수로 나뉘어졌다.

- 함수를 위상적으로 정렬하여 각 함수가 사용된 직후에 정의되어있다.(분석함수 -> 조합함수 순서)

- 코드를 리팩토링 하다보면 원래 했던 변경을 다시 되돌리는 경우가 흔하다.

- 보이스카웃 룰 (처음 왔을때마다 깨끗하게 만들려고 노력하라)