<5장> JUnit 5 기초
JUnit 5 구성요소
- JUnit 플랫폼 : 테스팅 프레임워크를 구동하기 위한 런처와 테스트 엔진을 위한 API 제공
- JUnit 주피터(Jupiter) : JUnit 5 를 위한 테스트 API 와 실행 엔진 제공
- JUnit 빈티지(Vintage) : JUnit 3 & 4로 작성된 테스트를 JUnit 5 플랫폼에서 실행하기 위한 모듈 제공
@Test & 테스트 메서드
@Test
- 테스트 실행할 메서드에 붙임
- 해당 메서드는 private 이면 안됨
주요 단언 메서드
Assertions 클래스가 제공하는 주요 단언 메서드
메서드 | 설명 |
assertEquals(expected, actual) | 실제 값(actual)이 기대하는 값(expected)과 같은지 검사 |
assertNotEquals(unexpected, 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() | 테스트를 실패 처리함 |
try {
AuthService authService = new AuthService();
authService.authenticate(null, null);
fail(); // 이 지점에 다다르면 fail() 메서드는 테스트 실패 에러 발생
} catch(IllegalArgumentException e) {
}
Assertions 가 제공하는 익셉션 발생 유무 검사 메서드
메서드 | 설명 |
assertThrows(Class<T> expectedType, Executable executable) | executable 을 실행한 결과로 지정한 타입의 익셉션이 발생하는지 검사 |
assertDoesNotThrow(Executable executable) | executable 을 실행한 결과로 익셉션이 발생하지 않는지 검사 |
assertThrows(IllegalArgumentException.class,
() -> {
AuthService authService = new AuthService();
authService.authenticate(null, null);
}
});
assertThrows()
- 발생한 익셉션 객체 리턴
- 발생한 익셉션을 이용 후 추가 검증이 필요하면 assertThrows() 메서드가 리턴한 익셉션 객체를 사용하면 됨
IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class,
() -> {
AuthService authService = new AuthService();
authService.authenticate(null, null);
}
});
assertTrue(thrown.getMessage().contains("id"));
Executable 인터페이스
- assertThrows() & assertDoesNotThrow() 에 사용
- execute() 메서드를 가진 함수형 인터페이스
public interface Executable {
void execute() throws Throwable;
}
assert 메서드
- 실패 시 다음 코드 실행하지 않고 바로 익셉션(AssertionFailedError) 발생
assertAll()
- 모든 검증 실행 후 그중 실패한 것이 있는지 확인하고 싶을 때 사용
assertAll(
() -> assertEquals(3, 5 / 2),
() -> assertEquals(4, 2 * 2),
() -> assertEquals(6, 11 / 2)
);
- Executable 목록을 가변 인자로 전달받아 각 Executable 실행
- 실행 결과로 검증에 실패한 코드가 있으면 그 목록을 모아 에러 메시지로 보여줌
테스트 라이프 사이클
@BeforeEach & @AfterEach
JUnit 을 활용한 각 테스트 메서드의 실행 순서
- 테스트 메서드를 포함한 객체 생성
- (존재하면) @BeforeEach 애노테이션이 붙은 메서드 실행
- @Test 애노테이션이 붙은 메서드 실행
- (존재하면) @AfterEach 애노테이션이 붙은 메서드 실행
public class LifecycleTest {
public LifecycleTest() {
System.out.println("new LifecycleTest");
}
@BeforeEach
void setUp() {
System.out.println("setUp");
}
@Test
void a() {
System.out.println("A");
}
@Test
void b() {
System.out.println("B");
}
@AfterEach
void tearDown() {
System.out.println("tearDown");
}
}
=== result ===
new LifecycleTest
setUp
A
tearDown
new LifecycleTest
setUp
B
tearDown
- @Test 메서드 실행 때마다 객체 새로 생성
- @BeforeEach
- @Test
- @AfterEach
@BeforeEach | @AfterEach |
테스트 실행하는데 필요한 준비 작업 시 사용 | 테스트 실행 후 정리할 것이 있을 때 사용 |
테스트에서 사용할 임시 파일 생성 or 테스트 메서드에서 사용할 객체 생성 | 테스트에서 사용한 임시 파일을 삭제해야 할 때 사용 |
@Test 애노테이션과 마찬가지로, 해당 애노테이션이 붙은 메서드는 private 이면 안 됨 |
@BeforeAll & @AfterAll
@BeforeAll | @AfterAll |
한 클래스의 모든 테스트 메서드가 실행되기 전, 특정 작업을 수행해야 할 때 사용 |
클래스의 모든 테스트 메서드를 실행한 뒤 실행됨 |
둘 다 정적 메서드에 붙이는데, 클래스의 모든 테스트 메서드를 실행하기 전 or 후에 한 번 실행됨 |
테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기
public class BadTest {
private FileOperator op = new FileOperator();
private static File file; // 두 테스트가 데이터를 공유할 목적으로 필드 사용
@Test
void fileCreationTest() {
File createdFile = op.createFile();
assertTrue(createdFile.length() > 0);
this.file = createdFile;
}
@Test
void readFileTest() {
long data = op.readData(file);
assertTrue(data > 0);
}
}
- fileCreationTest() 에서 생성한 File 을 보관 ➡️ file 필드를 readFileTest() 에 사용
- 테스트 메서드 실행할 때마다 객체를 새로 생성하기에, file 은 정적 필드로 정의
- 여기선 fileCreationTest() 가 readFileTest() 보다 먼저 실행되는 것을 가정함
테스트 메서드 실행 순서에 대한 고찰
- 테스트 메서드가 특정 순서대로 실행된다는 가정 하에 테스트 메서드를 작성하면 안 됨
- JUnit 이 테스트 순서를 결정하긴 하나, 그 순서는 버전에 따라 달라질 수 있음
- 위 예시에서 순서가 달라지면 테스트는 실패
- ➡️ 각 테스트 메서드는 서로 독립적으로 동작해야 함
- 한 테스트 메서드의 결과에 따라 다른 테스트 메서드의 실행 결과가 달라지면 안 됨
- 테스트 메서드가 서로 필드를 공유하거나 실행 순서를 가정한 테스트를 작성해서는 안 됨
@DisplayName
- 테스트에 표시 이름 붙이기
- 자바는 메서드 이름에 공백이나 특수 문자를 사용할 수 없기에, 메서드 이름만으로 테스트 내용을 충분히 설명하기 어려울 수 있음
@DisplayName("@DisplayName 테스트")
public class DisplayNameTest {
@DisplayName("값 같은지 비교")
@Test
void assertEqualsMethod() {
...
}
}
@Disabled
- 특정 테스트를 실행하고 싶지 않을 때 사용
- JUnit 은 @Disabled 붙은 클래스나 메서드는 테스트 실행 대상에서 제외함
- 테스트 코드가 미완성이거나 잠시 테스트를 실행하지 말아야 할 때 사용
모든 테스트 실행
- 개발할 때는 특정 테스트 클래스나 메서드만 실행
- ➡️ 원격 repo 에 푸시하거나 코드를 빌드해서 운영 환경에 배포하기 전, 모든 테스트를 실행해 깨지는 테스트가 없는지 확인
메이븐(Maven) | 그레이들(Gradle) |
mvn test (래퍼 사용한다면 mvnw test) | gradle test (래퍼 사용한다면 gradlew test) |
메이븐이 제공하는 라이프사이클에 따르면, package 단계 실행 시 test 단계를 앞서 실행하므로 mvn package 명령어를 실행해도 테스트 실행 | 그레이들도 build 테스크 실행 시 테스트 실행하므로, gradle build 명령어 실행하면 테스트 실행 |
<6장> 테스트 코드의 구성
상황 찾기
- 어떤 상황이 실행 결과에 영향을 줄 수 있는지 알아야
- 가능한 많은 예외 상황 찾기
- 소프트웨어 품질 향상에 중요
테스트 코드 구성 요소
- 상황 : given
- 실행 : when
- 결과 확인 : then
e.g. 1. 각 테스트 메서드마다 객체 생성해 상황 설정하기
@Test
void exactMatch() {
// given
BaseballGame game = new BaseballGame("456");
// when
Score score = game.guess("456");
// then
assertEquals(3, score.strikes());
assertEquals(0, score.balls());
}
@Test
void noMatch() {
// given
BaseballGame game = new BaseballGame("123");
// when
Score score = game.guess("456");
// then
assertEquals(0, score.strikes());
assertEquals(0, score.balls());
}
e.g. 2. @BeforeEach 적용해 상황 설정하기
private BaseballGame game;
@BeforeEach
void givenGame() {
game = new BaseballGame("456");
}
@Test
void exactMatch() {
// given
// when
Score score = game.guess("456");
// then
assertEquals(3, score.strikes());
assertEquals(0, score.balls());
}
e.g. 3. 상황이 없는 경우
- 결과에 영향을 주는 상황이 존재하지 않는 경우, 아래처럼 기능 실행 후 결 확인하는 코드만 포함하기도 함
@Test
void meetsAllCriteria_Then_Strong() {
// given
// when
PasswordStrengthMeter meter = new PasswordStrengthMeter();
PasswordStrength result = meter.meter("ab12!2AB");
// then
assertEquals(PasswordStrength.STRONG, result);
}
e.g. 4. 실행 결과로 익셉션 발생하는 경우
@Test
void genGame_With_DupNumber_Then_Fail() {
assertThrows(IllegalArgumentException.class,
() -> new BaseballGame("110");
);
}
외부 상황 & 외부 결과
- 외부 요인에 의한 상황을 테스트해야 할 때
- 테스트는 실행 때마다 동일한 결과를 보장해야 하는데, 우연에 의해 테스트 결과가 달라지면 동일한 결과 보장 x
e.g. 1. 파일이 없는 상황
@Test
void noDataFile_Then_Exception() {
givenNoFile("badpath.txt");
File dataFile = new File("badpath.txt");
assertThrows(IllegalArgumentException.class,
() -> MathUtils.sum(dataFile)
);
}
private void givenNoFile(String path) {
File file = new File(path);
if(file.exists()) {
boolean deleted = file.delete();
if(!deleted) {
throw new RuntimeException("fail givenNoFile: " + path);
}
}
}
e.g. 2. 파일이 존재하는 상황 : 상황에 알맞은 파일을 미리 만들기
@Test
void dataFileSumTest() {
File dataFile = new File("src/test/resources/datafile.txt");
long sum = MathUtils.sum(dataFile);
assertEquals(10L, sum);
}
- 다른 개발자도 테스트를 실행할 수 있어야 하기에, 테스트에 맞게 준비한 파일은 버전 관리 대상에 추가해야 함
e.g. 3. 파일이 존재하는 상황 : 파일을 미리 만들지 않고, 테스트 코드에서 상황에 맞는 파일 생성하기
@Test
void dataFileSumTest2() {
givenDataFile("target/datafile.txt", "1", "2", "3", "4");
File dataFile = new File("target/datafile.txt");
long sum = MathUtils.sum(dataFile);
assertEquals(10L, sum);
}
private void givenDataFile(String path, String... lines) {
try {
Path dataPath = Paths.get(path);
if(Files.exists(dataPath)) {
Files.delete(dataPath);
}
Files.write(dataPath, Arrays.asList(lines));
} catch(IOException e) {
throw new RuntimeException(e);
}
}
- 테스트 코드 안에 필요한 것이 다 있음
- 테스트 코드에서 상황을 명시적으로 구성하기에, 테스트 내용을 이해하기 위해 많은 파일을 볼 필요 없음
외부 상태가 테스트 결과에 영향을 주지 않게 하기
- 외부 상태에 따라 테스트 성공 여부가 바뀌지 않으려면, 테스트 실행 전에 외부를 원하는 상태로 만들거나 테스트 실행 후 외부 상태를 원래대로 되돌려놔야 함
- e.g. DB 데이터의 상태에 따라 테스트 결과가 달라지는 경우 : 중복 발생 방지 or 메서드 실행 후 트랜잭션을 롤백 등
외부 상태와 테스트 어려움
- 외부 요인 예시 : 파일, DBMS, 외부 서버 등에서 발생
- e.g. 1. API 응답 시간이 느릴 때
- e.g. 2. 실행 결과가 외부 시스템에 기록되는데, 데이터에 대해 INSERT, SELECT 권한만 주어질 때
- ➡️ 테스트 대상이 의존하는 대상의 실제 구현을 대신하는 구현인 '대역'을 사용하면 외부 상황이나 결과를 대체할 수 있어 테스트 작성이 쉬워짐
'Engineering' 카테고리의 다른 글
[테스트 주도 개발 시작하기] 11장 ~ 부록 (0) | 2023.06.22 |
---|---|
[테스트 주도 개발 시작하기] 9 ~ 10장 (0) | 2023.06.22 |
[테스트 주도 개발 시작하기] 7 ~ 8장 (0) | 2023.06.15 |
[테스트 주도 개발 시작하기] 3 ~ 4장 (0) | 2023.06.13 |
[테스트 주도 개발 시작하기] 1 ~ 2장 (0) | 2023.06.13 |
댓글