테스트 주도 개발로 배우는 객체 지향 설계와 실천 - 3장 도구 소개



Intro



초서-독서 한 내용을 그대로 적는 곳이기 때문에 책을 읽지 않은 분들이 보기에 맥락이 애매할 수 있습니다.

초서 : 책을 읽는데 그치는 것이 아닌 손을 이용해 책의 중요한 내용을 옮겨 적음으로써 능동적으로 책의 내용을 수용하고 판단하여 새로운 지식을 재창조하는 과정. 메타인지 학습법




Index





3장 도구 소개


JUnit 4 소개

기본적으로 JUnit 은 ‘리플렉션’ 을 통해 클래스 구조를 파악한 후 해당 클래스 내 테스트를 모두 실행한다.

public class CatalogTest {
    private final Catalog catalog = new Catalog(); // 픽스처
    
    @Test 
    public void containsAnAddedEntry() {    // 테스트 메서드는 값을 반환하거나 매개변수를 받아서는 안 됨.
        Entry entry = new Entry("사과", "바나나");
        catalog.add(entry);
        
        assertTrue(catalog.contains(entry));    // true 이므로 테스트 성공
    }
    
    @Test
    public void indexesEntriesByName() {
        Entry entry = new Entry("사과", "바나나");
        catalog.add(entry);
        
        assertEquals(entry, catalog.entryFor("사과"));    // 두 값이 동일하므로 (true) 이므로 테스트 성공
        assertNull(catalog.entryFor("멜론"));             // catalog 내에 '멜론'이 없으므로 (null) 테스트 성공
    }
}


테스트 케이스

  • @Test 애노테이션으로 지정된 메서드
  • 값을 반환하거나 매개변수를 받아서는 안된다.
  • JUnit 에서는 ‘매번’ 테스트 클래스의 ‘새 인스턴스를 생성한 후’ 테스트 케이스를 호출한다.
    • 매번 새 객체를 생성하면 각 테스트 간의 격리성이 확보됨.
    • 즉 각 테스트 케이스마다 객체 필드 내용을 마음껏 바꿀 수 있다.


Assertion

  • 테스트 내에서 테스트 대상 객체를 호출하고 그 결과를 Assertion(검사, 단정 등으로 불림) 한다.
  • JUnit 내 정의돼 있는 Assertion 메서드를 사용한다.
    • assertNull, assertEquals, assertThat 등…


예외 예상하기

  • @Test 애노테이션은 선택적인 매개변수로 expected를 지원한다.
  • 해당 테스트 케이스에서 던질 예외를 선언한다.
  • 테스트에서 예외를 던지지 않거나 다른 예외를 던지면 테스트 실패
@Test(expected=IllegalArgumentException.class)      // IllegalArgumentException 예외를 던지는지 검사
public void cannotAddTwoEntriesWithTheSameName() {  // 두 항목이 같은 이름으로 추가될 경우 예외
    catalog.add(new Entry("사과", "바나나"));
    catalog.add(new Entry("사과", "멜론"));
}


테스트 픽스처

  • 테스트가 시작할 때 존재하는 고정된 상태를 의미함.
  • 테스트가 반복 가능함을 보장 (매번 동일한 상태로 시작하므로 동일한 결과를 냄)
  • 준비(setup) 과정과 정리(teardown)과정이 있음.
  • @Before 애노테이션으로 픽스처를 준비하며 @After 애노테이션으로 픽스처를 정리.
    • 많은 JUnit 테스트에서 명시적으로 픽스처를 정리하지 않아도 되는데,
      픽스처를 준비할 때 JVM GC로 생성된 객체를 수거하는 것만으로도 충분하기 때문.
  • 공통적인 초기화 작업은 보통 @Before 메서드로 옮겨도 된다.
public class CatalogTest {
    final Catalog catalog = new Catalog();
    final Entry entry = new Entry("사과", "바나나");
    
    @Before
    public void fillTheCatalog() {
        catalog.add(entry);
    }
    
    ...
}


테스트 러너

  • JUnit 이 리플렉션을 수행해 해당 테스트를 실행하는 방식은 테스트 러너에서 제어한다.
  • @RunWith 애노테이션으로 설정.


햄크레스트 매처와 assertThat()

‘햄크레스트’ 는 매칭 조건을 선언적으로 작성하는 프레임워크이다.

  • 햄크레스트 자체는 테스트 프레임워크가 아니다.
  • 여러 테스트 프레임워크(JUnit, jMock, WindowLicker 등)에서 쓰인다.


햄크레스트 매처

  • 특정 객체가 어떤 조건과 일치하는지 알려준다.
  • 일치하지 않는 이유를 기술할 수 있다.
String str = "바나나가 먹고싶어요.";

Matcher<String> containsBananas = new StringContains("바나나");
Matcher<String> containsMangoes = new StringContains("망고");

assertTrue(containsBananas.matched(str));   // str 내 '바나나' 문자열이 있으므로 true, 테스트 성공
assertFalse(containsMangoes.matched(str));  // str 내 '망고' 문자열이 없으므로 false, 테스트 성공


  • 대게 매처는 직접적으로 인스턴스화 하지 않는다.
  • 코드 가독성을 높이고자 모든 매처에 대한 정적 팩터리 메서드 제공.
assertTrue(containsString("바나나").matched(str));    // str 내 '바나나' 문자열이 있으므로 true, 테스트 성공
assertFalse(containsString("망고").matched(str));     // str 내 '망고' 문자열이 없으므로 false, 테스트 성공
  • 코드가 조금 더 깔끔해 졌다.


  • 실제로는 매처를 JUnit assertThat 메서드와 조합해 사용한다.
    • assertThat 은 매처의 ‘자기서술적(self-describing)’ 인 특성을 활용해
      Assertion 에 실패할 경우 뭐가 잘못됐는지 분명하게 드러낼 수 있다.
assertThat(str, containsString("바나나"));        // str 내 '바나나' 문자열이 있으므로 테스트 성공
assertThat(str, not(containsString("망고")));     // str 내 '망고' 문자열이 없으므로 테스트 성공

assertThat(str, not(containsString("바나나")));    // str 내 '바나나' 문자열이 있으므로 테스트 실패
  • not 메서드는 전달된 매처의 의미와 반대되는 매처를 생성한다.
  • 마지막 구문의 실패 보고 내용은 다음과 같다.
java.lang.AssertionError:
Expected: not a string containing "바나나"
     got: "바나나가 먹고싶어요."
  • assertThat 에 매처 표현식을 전달하고 그 일을 알아서 처리하게 할 수 있다.
  • 사용자가 햄크레스트를 확장할 수 있다.
    • 매처 인터페이스를 팩터리 메서드로 구현하여 매처를 작성.
    • 결과물은 기존 매처 표현식과 자연스럽게 합쳐진다.


jMock2: 목 객체

jMock 은 목 객체를 활용한 테스트 방식을 지원한다.

  • 목 객체를 동적으로 생성함.
  • 목 구현체를 직접 작성하지 않는다.
  • 목 객체를 어떻게 호출하고 목 객체가 거기에 반응해 어떻게 동작할지 지정하는 고수준 API 제공.
  • ‘jMock 은 예상 구문의 기술을 최대한 명확하게 할 목적으로 고안한 것!!’


jMock 핵심 개념

  • 모조 객체
    • 테스트 대상 객체의 콘텍스트.
    • 대상을 이웃하는 객체(=그 객체가 사용하는 다른 객체)를 표현한다.
    • 즉 목 객체를 생성하기도 하고, 예상 구문을 정의하기도 하는 컨텍스트 이다.
  • 목 객체
    • 테스트 대상 객체의 실제 이웃(=그 객체가 사용하는 다른 객체, 연관 객체…)
    • 가짜 객체(프록시)같은 느낌
  • 예상 구문
    • 목 객체가 어떻게 호출(작동)하는지 기술하는 곳
    • 즉 목 객체 내부에 어떤 로직이 (테스트 대상 객체에 호출로 인해)어떻게 작동하는지 기술


/*
 * RunWith 애노테이션을 지정하면 
 * 테스트가 끝나는 시점에 모조 객체를 자동으로 호출해
 * 모든 목 객체가 예상대로 호출됐는지 검사함.
 */
@RunWith(JMock.class)
public class AuctionMessageTranslatorTest {
    
    /*
     * 모조 객체
     * 올바른 예외 타입을 던져 테스트 실패를 JUnit 에게 보고함.
     * 관례상 모조 객체를 context 라는 필드에 보관.
     * (테스트 대상 객체의 컨텍스트를 나타내므로)
     */ 
    private final Mockery context = new JUnit4Mockery();
    
    /*
     * 목 객체
     * 모조 객체를 사용해 목 객체를 생성한다.
     * 여기서 생성한 목 객체는 실제 리스너 구현체를 대신할 것이다.
     */
    private final AuctionEventListener listener = context.mock(AuctionEventListener.class);
    
    /*
     * 테스트 대상 객체
     * 목 객체(가짜 객체)를 해당 인스턴스의 생성자에 전달한다.
     * 목 객체의 인터페이스를 통해 상호 작용하며,
     * 해당 인터페이스가 어떻게 구현돼 있는지 신경 쓰지 않는다.
     */
    private final AuctionMessageTranslator translator = new AuctionMessageTranslator(listener);
    
    @Test
    public void notifiesAuctionClosedWhenCloseMessageReceived() {
        /*
         * 테스트에서 사용될 다른 객체
         */
        Message message = new Message();
        message.setBody("SOLVersion: 1.1; Event: CLOSE;");
        
        /*
         * 예상 구문 블록
         * 테스트 과정에서 번역기(translator)가 이웃 객체를 어떻게 호출할지 모조 객체에 알림.
         */
        context.checking(new Expectations() );
        
        /*
         * 테스트 대상 객체 호출
         * 테스트에 따르면 번역기는 리스너를 대상으로 auctionClosed()를 한 번 호출할 것으로 예상함.
         * 모조 객체는 테스트 과정에서 목 객체가 예상대로 호출됐는지 검사하고
         * 예상과 달리 호출되면 즉시 테스트가 실패하게 된다.
         */
        translator.processMessage(UNUSED_CHAT, message);
        
        /* 참고로 Assertion 을 아무것도 하지 않아도 된다. (목 객체 테스트에서 종종 볼 수 있음) */
    }
}


예상 구문

  • 예상하는 호출의 최소/최대 횟수
  • 호출이 예상되거나(호출되지 않을 경우 테스트 실패) 단순히 호출이 일어나는 것을 허용하는지 여부(호출되지 않으면 테스트 통과)
  • 매개변수 값 (상수로 만들어 지정하거나 햄크레스트 매처로 제한할 수 있음)
  • 다른 예상 구문을 고려한 제약 조건의 순서 지정
  • 반환할 값 지정
  • 던질 예외 지정


예상 구문 블록은 ‘이웃 객체가 어떻게 호출되는지 기술하는 코드’
‘실제도 객체를 호출하고 결과를 검사하는 코드’ 를 분명하게 구분하는 데 목적이 있다.
(선언적인 언어처럼 동작함)


” 가장 중요한 것은 구현 방법이 아니라 그 이면에 자리한 개념과 동기다. “