테스트 주도 개발로 배우는 객체 지향 설계와 실천 - 3장 도구 소개
31 Mar 2021Intro
초서-독서 한 내용을 그대로 적는 곳이기 때문에 책을 읽지 않은 분들이 보기에 맥락이 애매할 수 있습니다.
초서 : 책을 읽는데 그치는 것이 아닌 손을 이용해 책의 중요한 내용을 옮겨 적음으로써 능동적으로 책의 내용을 수용하고 판단하여 새로운 지식을 재창조하는 과정. 메타인지 학습법
Index
- 들어가면서
- 1부 서론
- 1장 테스트 주도 개발의 핵심은 무엇인가?
- 2장 객체를 활용한 테스트 주도 개발
- 3장 도구 소개 «
- 2부 테스트 주도 개발 과정
- 4장 테스트 주도 주기 시작
- 5장 테스트 주도 개발 주기의 유지
- 6장 객체 지향 스타일
- 7장 객체 지향 설계의 달성
- 8장 서드 파티 코드를 기반으로 한 개발
- 3부 동작하는 예제
- 9장 경매 스나이퍼 개발 의뢰
- 10장 동작하는 골격
- 11장 첫 테스트 통과하기
- 12장 입찰 준비
- 13장 스나이퍼가 입찰하다
- 14장 스나이퍼가 경매에서 낙찰하다
- 15장 실제 사용자 인터페이스를 향해
- 16장 여러 품목에 대한 스나이핑
- 17장 Main 분석
- 18장 세부 사항 처리
- 19장 실패 처리
- 4부 지속 가능한 테스트 주도 개발
- 20장 테스트에 귀 기울이기
- 21장 테스트 가독성
- 22장 복잡한 테스트 데이터 만들기
- 23장 테스트 진단
- 24장 테스트 유연성
- 5부 고급 주제
- 25장 영속성 테스트
- 26장 단위 테스트와 스레드
- 27장 비동기 코드 테스트
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로 생성된 객체를 수거하는 것만으로도 충분하기 때문.
- 많은 JUnit 테스트에서 명시적으로 픽스처를 정리하지 않아도 되는데,
- 공통적인 초기화 작업은 보통
@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 을 아무것도 하지 않아도 된다. (목 객체 테스트에서 종종 볼 수 있음) */
}
}
예상 구문
- 예상하는 호출의 최소/최대 횟수
- 호출이 예상되거나(호출되지 않을 경우 테스트 실패) 단순히 호출이 일어나는 것을 허용하는지 여부(호출되지 않으면 테스트 통과)
- 매개변수 값 (상수로 만들어 지정하거나 햄크레스트 매처로 제한할 수 있음)
- 다른 예상 구문을 고려한 제약 조건의 순서 지정
- 반환할 값 지정
- 던질 예외 지정
예상 구문 블록은 ‘이웃 객체가 어떻게 호출되는지 기술하는 코드’ 와
‘실제도 객체를 호출하고 결과를 검사하는 코드’ 를 분명하게 구분하는 데 목적이 있다.
(선언적인 언어처럼 동작함)
” 가장 중요한 것은 구현 방법이 아니라 그 이면에 자리한 개념과 동기다. “
- 들어가면서
- 1부 서론
- 1장 테스트 주도 개발의 핵심은 무엇인가?
- 2장 객체를 활용한 테스트 주도 개발
- 3장 도구 소개 «
- 2부 테스트 주도 개발 과정
- 4장 테스트 주도 주기 시작
- 5장 테스트 주도 개발 주기의 유지
- 6장 객체 지향 스타일
- 7장 객체 지향 설계의 달성
- 8장 서드 파티 코드를 기반으로 한 개발
- 3부 동작하는 예제
- 9장 경매 스나이퍼 개발 의뢰
- 10장 동작하는 골격
- 11장 첫 테스트 통과하기
- 12장 입찰 준비
- 13장 스나이퍼가 입찰하다
- 14장 스나이퍼가 경매에서 낙찰하다
- 15장 실제 사용자 인터페이스를 향해
- 16장 여러 품목에 대한 스나이핑
- 17장 Main 분석
- 18장 세부 사항 처리
- 19장 실패 처리
- 4부 지속 가능한 테스트 주도 개발
- 20장 테스트에 귀 기울이기
- 21장 테스트 가독성
- 22장 복잡한 테스트 데이터 만들기
- 23장 테스트 진단
- 24장 테스트 유연성
- 5부 고급 주제
- 25장 영속성 테스트
- 26장 단위 테스트와 스레드
- 27장 비동기 코드 테스트