테스트 주도 개발 시작하기 - chapter5 ~ chapter7
5. JUnit5 기초
5-1. JUnit 모듈 구성
- JUnit5의 구성 요소
ㆍJUnit 플랫폼 : 테스팅 프레임워크를 구동하기 위한 런처와 테스트 엔진을 위한 API 제공
ㆍJUnit 주피터(Jupiter) : JUnit5를 위한 테스트 API와 실행 엔진을 제공한다.
ㆍJUnit 빈티지(Vintage) : JUnit3과 4로 작성된 테스트를 JUnit5 플랫폼에서 실행하기 위한 모듈을 제공한다.
5-2. @Test 애노테이션과 테스트 메서드
- JUnit 코드의 기본 구조는 간단하다. 테스트로 사용할 클래스를 만들고 @Test 애노테이션을 메서드에 붙이기만 하면 된다.
- JUnit의 Assertions 클래스는 검증하기 위한 목적의 다양한 정적 메서드를 제공한다.
5-3. 주요 단언 메서드
- Assertions 클래스가 제공하는 주요 단언 메서드
메서드 | 설명 |
assertEquals(expected, actual) | 실제 값(actual)이 기대하는 값(expected)과 같은지 검사한다. |
assertNotEquals(unexpected, actual) | 실제 값(actual)이 특정 값(unexpected)과 같지 않은지 검사한다. |
assertSame(Object expected, Object actual) | 두 객체가 동일한 객체인지 검사한다. |
assertNotSame(Object unexpected, Object actual) | 두 객체가 동일하지 않은 객체인지 검사한다. |
assertTrue(boolean condition) | 값이 true인지 검사한다. |
assertFalse(boolean condition) | 값이 false인지 검사한다. |
assertNull(Object actual) | 값이 null인지 검사한다. |
assertNotNull(Object actual) | 값이 null이 아닌지 검사한다. |
fail() | 테스트를 실패처리한다. |
- Assertions가 제공하는 익셉션 발생 유무 검사 메서드
메서드 | 설명 |
assertThrows(Class<T> expectedType, Executable executable) |
executable을 실행한 결과로 지정한 타입의 익셉션이 발생하는지 검사한다. |
assertDoesNotThrow(Executable executable) | executable을 실행한 결과로 익셉션이 발생하지 않는지 검사한다. |
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> {
AuthService authService = new AuthService();
authService.authenticate(null, null);
});
assertTrue(thrown.getMessage().contains("id"));
- 모든 검증을 실행하고 그 중에 실패한 것이 있는지 확인하고 싶을 때
assertEquals(3, 5/2); // 검증 실패로 에러 발생
assertEquals(4, 2*2); // 이 코드는 실행되지 않음
assertAll(
() -> assertEquals(3, 5/2);
() -> assertEquals(4, 2*2);
() -> assertEquals(6, 11/2);
)
5-4. 테스트 라이프 사이클
@BeforeEach 애노테이션과 @AfterEach 애노테이션
- JUnit은 각 테스트 메서드마다 다음 순서대로 코드를 실행
ㆍ1. 테스트 메서드를 포함한 객체 생성
ㆍ2. (존재하면) @BeforeEach 애노테이션이 붙은 메서드 실행
ㆍ3. @Test 애노테이션이 붙은 메서드 실행
ㆍ4. (존재하면) @AfterEach 애노테이션이 붙은 메서드 실행
@BeforeAll 애노테이션과 @AfterAll 애노테이션
- 한 클래스의 모든 클래스 메서드가 실행되기 전에 특정 작업을 수행한다면 @BeforeAll 애노테이션 사용
- @BeforeAll 애노테이션은 정적 메서드를 붙이는데 이 메서드는 클래스의 모든 테스트 메서드를 실행하기 전에 한 번 실행된다.
- @AfterAll 애노테이션은 반대로 클래스의 모든 테스트 메서드를 실행한 뒤에 실행된다.
- 이 메서드 역시 정적 메서드에 적용한다.
5-5. 테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기
- 테스트 메서드가 특정 순서대로 실행된다는 가정하에 테스트 메서드를 작성하면 안된다.
- 각 테스트 메서드는 서로 독립적으로 동작해야 한다.
5-6. 추가 애노테이션: @DisplayName, @Disabled
- @DisplayName 애노테이션을 사용해서 테스트에 표시 이름을 붙일 수 있다.
- JUnit은 @Disabled 애노테이션이 붙은 클래스나 메서드는 테스트 실행 대상에서 제외한다.
5-7. 모든 테스트 실행하기
- mvn test (래퍼를 사용하는 경우 mvnw test) & mvn package 명령어
- gradle test (래퍼를 사용하는 경우 gradlew test) & gradle build 명령어
- 인텔리제이는 src/test/java 폴더에서 테스트 실행
6. 테스트 코드의 구성
6-1. 기능에서의 상황
- 어떤 상황이 실행 결과에 영향을 줄 수 있는지 찾기 위해 노력한다.
- 다양한 예외 상황을 찾아내고 이를 코드에 반영해야 기능이 비정상적으로 동작하는 것을 막을 수 있다.
6-2. 테스트 코드의 구성 요소: 상황, 실행, 결과 확인
- 어떤 상황이 주어지고, 그 상황에서 기능을 실행하고, 실행한 결과를 확인하는 세 가지가 테스트 코드의 기본 골격을 이루게 된다.
ㆍ상황, 실행, 결과 확인은 영어 표현 given when, then 에 대응한다.
6-3. 외부 상황과 외부 결과
외부 상태가 테스트 결과에 영향을 주지 않게 하기
- 외부 상태에 따라 테스트의 성공 여부가 바뀌지 않으려면 테스트 실행 전에 외부를 원하는 상태로 만들거나 테스트 실행 후에 외부 상태로 원래대로 되돌려 놓아야 한다.
외부 상태와 테스트 어려움
- 상황과 결과에 영향을 주는 외부 요인은 파일, DBMS, 외부 서버 등 다양하다.
- 테스트 대상의 상황과 결과에 외부 요인이 관여할 경우 대역을 사용하면 테스트 작성이 쉬워진다.
ㆍ대역은 테스트 대상이 의존하는 대상의 실제 구현을 대신하는 구현인데 이 대역을 통해서 외부 상황이나 결과를 대체할 수 있다.
7. 대역
7-1. 대역의 필요성
- 테스트를 작성하다 보면 외부 요인이 필요한 시점이 있다.
ㆍ테스트 대상에서 파일 시스템을 사용
ㆍ테스트 대상에서 DB로부터 데이터를 조회하거나 데이터를 추가
ㆍ테스트 대상에서 외부의 HTTP 서버와 통신
- 대역에는 종류는 스텁, 가짜, 스파이, 모의 객체가 존재하는데 각 대역 종류마다 쓰임새가 다르다.
7-2. 대역을 이용한 테스트
7-3. 대역을 사용한 외부 상황 흉내와 결과 검증
7-4. 대역의 종류
대역 종류 | 설명 |
스텁(Stub) | 구현을 단순한 것으로 대체한다. 테스트에 맞게 단순히 원하는 동작을 수행한다. StubCardNumberValidator가 스텁 대약에 해당한다. |
가짜(Fake) | 제품에는 적합하지 않지만, 실제 동작하는 구현을 제공한다. DB 대신에 메모리를 이용해서 구현한 MemoryAutoDebitInfoRepository가 가짜 대역에 해당한다. |
스파이(Spy) | 호출된 내역을 기록한다. 기록한 내용은 테스트 결과를 검증할 때 사용한다. 스텁이기도 하다 |
모의(Mock) | 기대한 대로 상호작용하는지 행위를 검증한다. 기대한 대로 동작하지 않으면 익셉션을 발생할 수 있다. 모의 객체는 스텁이자 스파이도 된다 |
7-5. 상황과 결과 확인을 위한 협업 대상(의존) 도출과 대역 사용
7-6. 대역과 개발 속도
- 대역을 사용하면 실제 구현이 없어도 다양한 상황에 대해 테스트할 수 있다.
7-7. 모의 객체를 과하게 사용하지 않기
- 모의 객체를 이용하면 대역 클래스를 만들지 않아도 되니깐 처음에는 편할 수 있다.
ㆍ하지만 결과 값을 확인하는 수단으로 모의 객체를 사용하기 시작하면 결과 검증 코드가 길어지고 복잡해진다.
- 특히 DAO나 리포지토리와 같이 저장소에 대한 대역은 모의 객체를 사용하는 것보다 메모리를 이용한 가짜 구현을 사용하는 것이 테스트 코드 관리에 유리하다.
- 물론 처음에는 가짜 대역을 구현해야 하니깐 귀찮을 수 있는데 일단 가짜 대역을 구현하면 모의 객체를 사용할 때보다 테스트 코드가 간결해지고 관리하기 쉬워진다.