[Book - JUnit IN ACTION 3판] 2. JUnit 핵심 살펴보기
프로그램이 점점 커지면서 새로운 클래스와 메서드가 계속 추가된다면 새로운 테스트 코드도 그에 맞춰 추가되어야 한다.
크래스들은 때로 누구도 생각하지 못한 방식으로 상호작용할 수 있기 때문에 어떤 코드가 변경됐는지와 상관없이 언제든 모든 테스트를 실행할 수 있어야 한다.
그런데 어떻게 여러 테스트를 실행할 수 있을까? 어떤 테스트를 통과했고 실패했는지 어떻게 알 수 있을까?
JUnit을 이용해서 테스트를 실행하고 테스트 결과를 확인하는 방법을 알아보자.
핵심 애노테이션
- 테스트 클래스(test class)
- 클래스, 정적 멤버 클래스, 하나 이상의 테스트 메서드를 포함하는
@Nested
애노테이션이 붙은 내부 클래스를 말한다. - 테스트 클래스는 추상 클래스일 수 없으며, 단일한 생성자(오직 하나의 인스턴스)를 가지고 있어야 한다.
- 테스트 클래스에 생성자를 생성하지 않았어도 Java Compiler가 기본 생성자를 생성해준다.
- 클래스, 정적 멤버 클래스, 하나 이상의 테스트 메서드를 포함하는
- 테스트 메서드(test method)
- 테스트 메서드는 추상 메서드일 수 없으며, 반환 값을 가질 수 없다. 즉, 반환 타입은 반드시 void여야 한다.
- 종류
@Test
@RepeatedTest
@ParameterizedTest
@TestFactory
@TestTemplate
- 생애 주기 메서드(life cycle method)
- 종류
@BeforeAll
@AfterAll
@BeforeEach
@AfterEach
- 종류
JUnit은 테스트 메서드의 격리성을 보장하고 테스트 코드에서 의도치 않은 부수 효과를 방지하기 위해,
@Test
메서드를 호출하기 전에 테스트 클래스 인스턴스를 매번 새로 만든다.
테스트는 실행 순서에 관계없이 동일한 결과를 얻을 수 있어야 하는 것이 당연하기 때문이다.
따라서 각 테스트 메서드는 매번 새로 만들어진 테스트 클래스 인스턴스에서 실행되므로 테스트 메서드 간에 인스턴스 변수를 재사용할 수는 없다.
대신 테스트 클래스에 @TestInstance(Lifecycle.PER_CLASS)
애노테이션을 추가하면 테스트 클래스 인스턴스가 메서드 단위가 아닌 클래스 단위로 생성된다.
생애 주기 메서드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import chapter2.ResourceForAllTests;
import chapter2.SUT;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class SUTTest {
private static ResourceForAllTests resourceForAllTests;
private SUT systemUnderTest;
@BeforeAll
static void setUpClass() {
resourceForAllTests = new ResourceForAllTests("테스트를 위한 리소스");
}
@AfterAll
static void tearDownClass() {
resourceForAllTests.close();
}
@BeforeEach
void setUp() {
systemUnderTest = new SUT("테스트 대상 시스템");
}
@AfterEach
void tearDown() {
systemUnderTest.close();
}
@Test
void testRegularWork() {
boolean canReceiveRegularWork = systemUnderTest.canReceiveRegularWork();
assertTrue(canReceiveRegularWork);
}
@Test
void testAdditionalWork() {
boolean canReceiveAdditionalWork = systemUnderTest.canReceiveAdditionalWork();
assertFalse(canReceiveAdditionalWork);
}
}
@BeforeAll
- 전체 테스트가 실행되기 전에 한번 실행된다.
- 이 애노테이션이 붙은 메서드는 테스트 클래스에
@TestInstance(Lifecycle.PER_CLASS)
애노테이션이 없다면 static으로 선언해야 한다.
@BeforeEach
- 각 테스트가 실행되기 전에 실행된다.
- 위 코드에서는 테스트 메서드가 2개 있으므로 총 2번 실행된다.
@Test
- 테스트 메서드 2개는 서로 간에 독립적으로 실행된다.
@AfterEach
- 이 애노테이션이 붙은 메서드는 각 테스트가 실행된 이후에 실행된다.
- 위 코드에서는 테스트 메서드가 2개 있으므로 총 2번 실행된다.
@AfterAll
- 전체 테스트가 실행된 후 한 번 실행된다.
- 이 애노테이션이 붙은 메서드는 테스트 클래스에
@TestInstance(Lifecycle.PER_CLASS)
애노테이션이 없다면 static으로 선언해야 한다.
@DisplayName
@DisplayName
애노테이션은 테스트 클래스, 테스트 메서드에서 사용할 수 있다.
개발자가 테스트 클래스나 테스트 메서드에 자신만의 디스플레이명을 작성하는 데 사용한다.
테스트 목적을 알려 줄 수 있는 완전한 문장 수준으로 적는 것이 일반적이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DisplayName("Test class showing the @DisplayName annotation.")
public class DisplayNameTest {
private final SUT systemUnderTest = new SUT();
@Test
@DisplayName("Our system under test says hello.")
void testHello() {
assertEquals("Hello", systemUnderTest.hello());
}
@Test
@DisplayName("😁")
void testTalking() {
assertEquals("How are you?", systemUnderTest.talk());
}
@Test
void testBye() {
assertEquals("Bye", systemUnderTest.bye());
}
}
- 전체 테스트 클래스에 적용할 디스플레이명을 작성할 수 있다.
- 일반적인 텍스트로 디스플레이명을 작성할 수 있다.
- 디스플레이명에 이모지를 쓸 수도 있다.
- 디스플레이명을 명시하지 않은 테스트는 메서드명이 표시된다.
만약 IntelliJ 환경에서 테스트를 했을 때 디스플레이명이 나오지 않는다면
IntelliJ IDEA > Settings.. > Build, Execution, Deployment > Build Tools > Gradle에서
Run tests using을 Gradle이 아닌 IntelliJ IDEA로 변경해보자.
Disabled
@Disabled
애노테이션은 테스트 클래스나 테스트 메서드에서 사용할 수 있다.
@Disabled
를 붙인 테스트 클래스나 테스트 메서드는 비활성화되므로 테스트가 실행되지 않는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import chapter2.lifecycle.SUT;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Disabled("기능 개발 중")
public class DisabledMethodsTest {
private final SUT systemUnderTest = new SUT("테스트 대상 시스템");
@Test
void testRegularWork() {
boolean canReceiveRegularWork = systemUnderTest.canReceiveRegularWork();
assertTrue(canReceiveRegularWork);
}
@Test
void testAdditionalWork() {
boolean canReceiveAdditionalWork = systemUnderTest.canReceiveAdditionalWork();
assertFalse(canReceiveAdditionalWork);
}
}
@Disabled
의 파라미터로 테스트를 비활성화한 이유를 적는 방법은 실무에서 자주 쓰인다.- 위 코드는 테스트 클래스에
@Disabled
를 사용했지만 동일하게 테스트 메서드에도 사용할 수 있다.
중첩 테스트
두 개의 클래스가 지나치게 결합도가 높다면 내부 클래스와 외부 클래스로 만들어 내부 클래스에서 외부 클래스의 모든 인스턴스 변수에 접근할 수 있도록 해 주는 것이 합리적이다.
중첩 테스트는 개발자가 비즈니스 로직을 잘 따르게 하는 한편,
분명한 테스트 코드를 작성하도록 유도하여 개발자가 테스트 프로세스에 더욱 자연스럽게 적응하도록 만든다.
결합도 관점에서 중첩 테스트는 개발자가 테스트 그룹 간의 관계를 표현하는 데에도 도움이 된다.
참고로 내부 클래스는 해당 패키지 내에서만 접근이 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class NestedTestsTest {
private static final String FIRST_NAME = "John";
private static final String LAST_NAME = "Smith";
@Nested
class BuilderTest {
private final String MIDDLE_NAME = "Michael";
@Test
void customerBuilder() throws ParseException {
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("MM-dd-yyyy");
Date customerDate = simpleDateFormat.parse("04-21-2019");
Customer customer = Customer.builder()
.gender(Gender.MALE)
.firstName(FIRST_NAME)
.lastName(LAST_NAME)
.middleName(MIDDLE_NAME)
.becomeCustomer(customerDate)
.build();
assertAll(() -> {
assertEquals(Gender.MALE, customer.getGender());
assertEquals(FIRST_NAME, customer.getFirstName());
assertEquals(LAST_NAME, customer.getLastName());
assertEquals(MIDDLE_NAME, customer.getMiddleName());
assertEquals(customerDate, customer.getBecomeCustomer());
});
}
}
}
- 메인 테스트는
NestedTestsTest
이며, 중첩 테스트BuilderTest
와 결합되어 있다. - 먼저
NestedTestsTest
는 모든 중첩 테스트에 사용할 고객의 이름과 성을 선언한다. - 중첩 테스트인
BuilderTest
는 빌더 패턴을 활용하여 Customer 객체를 제대로 생성했는지 검증한다. - 각 필드의 값이 동등하지 아닌지는 customer Builder 테스트가 끝날 때 확인할 수 있다.
책에서는 빌더 패턴을 직접 구현했지만, 나는 롬복 애노테이션을 이용했다.
태그를 사용한 테스트
실무에서는 테스트를 몇몇 카테고리로 범주화할 때 태그를 사용한다.
비즈니스 로직이나 기타 기준으로 태그를 달아 테스트를 그룹으로 묶을 수 있다.(각 테스트 카테고리는 고유한 태그가 있다.)
그다음 상황에 맞게 특정 카테고리의 테스트만 수행하거나 대상 태그를 다른 것으로 바꿀 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Tag("repository")
public class CustomerRepositoryTest {
private String CUSTOMER_NAME = "John Smith";
private CustomerRepository repository = new CustomerRepository();
@Test
void testNonExistence() {
boolean exists = repository.contains("John Smith");
assertFalse(exists);
}
@Test
void testCustomerPersistence() {
repository.persist(new Customer(CUSTOMER_NAME));
assertTrue(repository.contains("John Smith"));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
@Tag("individual")
public class CustomerTest {
private final String CUSTOMER_NAME = "John Smith";
@Test
void testCustomer() {
Customer customer = new Customer(CUSTOMER_NAME);
assertEquals("John Smith", customer.getName());
}
}
위 코드들과 같이 @Tag("individual")
태그와 @Tag("repository")
태그를 작성하고,
아래와 코드를 build.gradle에 추가하면
1
2
3
4
5
6
test {
useJUnitPlatform {
includeTags 'individual'
excludeTags 'repository'
}
}
@Tag("individual")
태그가 지정된 테스트는 제외되고,
@Tag("repository")
태그가 지정된 테스트만 실행되는 것을 알 수 있다.
단언문
결과값을 검증하려면 JUnit 5의 Assertions 클래스에서 제공하는 단언문 메서드를 사용해야 한다.
assertAll
executable 객체의 컬렉션을 assertAll 메서드의 파라미터를 사용해서
executable 객체가 예외를 던지지 않는다는 것을 더 간결하고 편리하게 단어해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
public class AssertAllTest {
@Test
@DisplayName("기본적으로 테스트 대상 시스템은 검증하지 않는다")
void testSystemNotVerified() {
SUT systemUnderTest = new SUT("테스트 대상 시스템");
assertAll("테스트 대상 시스템을 검증하지 않았는지 확인",
() -> assertEquals("테스트 대상 시스템", systemUnderTest.getSystemName()),
() -> assertFalse(systemUnderTest.isVerified()));
}
@Test
@DisplayName("테스트 대상 시스템을 검증한다")
void testSystemUnderVerification() {
SUT systemUnderTest = new SUT("테스트 대상 시스템");
systemUnderTest.verify();
assertAll("테스트 대상 시스템을 검증했는지 확인",
() -> assertEquals("테스트 대상 시스템", systemUnderTest.getSystemName()),
() -> assertTrue(systemUnderTest.isVerified()));
}
}
assertAll 메서드의 좋은 점은 일부 단언문이 실패하더라도 모든 단언문을 항상 검증한다는 것이다.
앞의 executable 객체가 실패하더라도 뒤의 객체가 실행된다.
JUnit 4에서는 여러 개 assert 메서드 중에 하나가 실패한다면 그 실패로 인해 전체 메서드가 중단된다.
- 첫 번째 테스트
- assertAll 메서드는 executable 객체 중 하나가 예외를 던지는 경우 표시할 메시지를 파라미터로 작성했다.
- 그 다음 assertEquals 메서드로 검증할 executable 객체와 assertFalse 메서드로 검증할 executable 객체를 전달한다.
- 단언문 조건은 한눈에 들어올 수 있도록 간결하게 적었다.
- 두 번째 테스트
- assertAll 메서드는 executable 객체 중 하나가 예외를 던지는 경우 표시할 메시지를 파라미터로 작성했다.
- 그다음 assertEquals 메서드로 검증할 executable 객체와 assertTrue 메서드로 검증할 executable 객체를 전달한다.
- 첫 번째 테스트와 마찬가지로 단언문의 조건은 읽기 쉽게 작성되어 있다.
Supplier<String>
Supplier<String>을 사용하면 테스트가 성공했을 때 오류 메시지를 만들지 않는다.
이런 식으로 람다식이나 메서드 참조를 사용하여 시스템을 검증하면 성능을 조금 높일 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
@Test
@DisplayName("테스트 대상 시스템을 검증한다")
void testSystemUnderVerification() {
systemUnderTest.verify();
assertTrue(systemUnderTest.isVerified(),
() -> "테스트 대상 시스템을 검증했는지 확인");
}
@Test
@DisplayName("테스트 대상 시스템을 검증하지 않았다")
void testSystemNotUnderVerification() {
assertFalse(systemUnderTest.isVerified(),
() -> "테스트 대상 시스템을 검증하지 않았는지 확인");
}
@Test
@DisplayName("현재 테스트 대상 시스템은 작업이 없다")
void testNoJob() {
assertNull(systemUnderTest.getCurrentJob(),
() -> "테스트 대상 시스템은 현재 작업이 없는지 확인");
}
- 첫 번째 테스트
- assertTrue 메서드를 사용하여 조건이 참인지 검증한다.
- 실패하면 “테스트 대상 시스템을 검증했는지 확인”이 지연 전달된다.
- 두 번째 테스트
- assertFalse 메서드를 사용하여 조건이 거짓인지 검증한다.
- 실패하면 “테스트 대상 시스템을 검증하지 않았는지 확인”이 지연 전달된다.
- 세 번째 테스트
- 객체가 존재하는지는 assertNull 메서드를 가지고 검증할 수 있다.
- 실패하면 “테스트 대상 시스템은 현재 작업이 없는지 확인”이 지연 전달된다.
즉, 단언문에서 람다식을 파라미터로 사용할 때의 이점은 지연 전달 덕분에 성능이 향상된다는 데 있다.
assertTimeout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static java.time.Duration.ofMillis;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
public class AssertTimeoutTest {
private final SUT systemUnderTest = new SUT("테스트 대상 시스템");
@Test
@DisplayName("작업을 마칠 때까지 기다리는 assertTimeout 메서드")
void testTimeout() throws InterruptedException {
systemUnderTest.addJob(new Job("Job 1"));
assertTimeout(ofMillis(500),
() -> systemUnderTest.run(200));
}
@Test
@DisplayName("시간이 지나면 작업을 중지시키는 assertTimeoutPreemptively 메서드")
void testTimeoutPrremptively() throws InterruptedException {
systemUnderTest.addJob(new Job("Job 1"));
assertTimeoutPreemptively(ofMillis(500),
() -> systemUnderTest.run(200));
}
}
- assertTimeout
- executable 객체가 작업을 마칠 때까지 기다린다.
- 만약 테스트가 주어진 시간을 초과하면 execution exceeded timeout of 500 ms by 100ms와 같은 메시지로 테스트가 얼마나 늦어졌는지 알려 준다.
- assertTimeoutPreemptively
- 시간이 지나면 executable 객체를 중지시킨다.
- 만약 테스트가 실패한다면 execution timed out after 500 ms와 같이 지정한 시간 안에 테스트가 완료되지 못했다고 알려 준다.
assertThrows
assertThrows 메서드는 예외가 발생했을 때 Throwable 객체를 반환한다.
반환한 Throwable 객체를 단언문으로 검증할 수 있다.
이렇게 하면 시스템에서 할당된 Job 객체가 없을 때 예외를 던지는지 확인할 수 있으므로 테스트 가독성이 좋아진다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class AssertThrowsTest {
private final SUT systemUnderTest = new SUT("테스트 대상 시스템");
@Test
@DisplayName("예외가 발생하는지 검증한다")
void testExpectedException() {
assertThrows(NoJobException.class, systemUnderTest::run);
}
@Test
@DisplayName("예외가 발생하고 예외에 대한 참조가 유지되는지 검증한다")
void testCatchException() {
Throwable throwable = assertThrows(NoJobException.class,
() -> systemUnderTest.run(1000));
assertEquals("테스트 대상 시스템은 현재 작업이 없는지 확인",
throwable.getMessage());
}
}
- systemUnderTest에서 run 메서드를 호출했을 때 NoJobException이 발생하는지 검증한다.
- systemUnderTest.run(1000) 문장이 NoJobException을 던지는지 검증한다.
- throwable에 예외에 대한 참조가 유지되었는지도 검증한다.
- throwable이 가지고 있는 에러 메시지가 “테스트 대상 시스템은 현재 작업이 없는지 확인”인지 검증한다.
가정문
외부 환경이나 우리가 제어할 수 없는 날짜, 시간대 같은 문제 탓에 테스트가 실패할 수도 있다.
이럴 때 가정문(assumption)을 사용한다면 부적절한 조건에서 테스트가 실행되는 것을 사전에 막을 수 있다.
가정문은 테스트를 수행하는 데 필수인 전제 조건이 충족되었는지를 검증하는 문장이다.
테스트를 계속 진행하는 것이 합리적이지 않을 수 있다고 생각되는 부분에 가정문을 사용할 수 있다.
참고록 테스트 리포트에서 가정문에 의해 중단된 테스트는 실패한 것으로 처리한다.
JUnit 5는 자바 8 람다식과 함께 사용할 수 있는 가정문 메서드를 지원한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import org.example.junitinaction3.chapter02.assumptions.environment.JavaSpecification;
import org.example.junitinaction3.chapter02.assumptions.environment.OperationSystem;
import org.example.junitinaction3.chapter02.assumptions.environment.TestsEnvironment;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
public class AssumptionsTest {
private static final String EXPECTED_JAVA_VERSION = "1.8";
private final TestsEnvironment environment = new TestsEnvironment(
new JavaSpecification(
System.getProperty("java.vm.specification.version")),
new OperationSystem(
System.getProperty("os.name"),
System.getProperty("os.arch")
)
);
private final SUT systemUnderTest = new SUT();
@BeforeEach
void setUp() {
assumeTrue(environment.isWindows());
}
@Test
void testNoJobToRun() {
assumingThat(
() -> environment.getJavaVersion().equals(EXPECTED_JAVA_VERSION),
() -> assertFalse(systemUnderTest.hasJobToRun())
);
}
@Test
void testJobToRun() {
assumeTrue(environment.isAmd64Architecture());
systemUnderTest.run(new Job());
assertTrue(systemUnderTest.hasJobToRun());
}
}
@BeforeEach
애노테이션이 붙은 메서드는 각 테스트가 실행되기 전에 실행된다.- 현재 OS 환경이 윈도우라는 가정을 만족하지 않으면 테스트가 실행되지 않는다.
- 첫 번째 테스트는 현재 자바 버전이 1.8인지 검증한다.
- 자바 버전이 1.8일 때만 시스템에서 현재 실행 중인 작업이 없음을 검증한다.
- 두 번째 테스트는 현재 아키텍처가 사전에 가정한 환경인지 검증한다.
- 아키텍처가 AMD64인 경우에만 시스템에서 새로운 작업을 수행한다.
- 그리고 시스템에 실행할 작업이 있는지 검증한다.
나는 Mac OS라 @BeforeEach
에서의 가정을 만족하지 않아서 테스트가 실행되지 않는다.
그래서 @BeforeEach
메서드를 주석 처리하고 테스트하면,
첫 번째 테스트에서는 자바 21을 사용중이라 자바 1.8을 쓰는지에 대한 가정문을 만족하지 않아 테스트가 실행되지 않고,
두 번째 테스트는 통과했다.
JUnit 5의 의존성 주입
JUnit 5부터는 생성자와 메서드에서 파라미터를 가질 수 있도록 허용하지만,
의존성 주입으로 해결해야 한다는 점이 다르다.
ParameterResolver 인터페이스는 런타입에 파라미터를 동적으로 리졸브한다.
생성자나 메서드의 파라미터는 사전에 등록한 ParameterResolver에 의해 런타임에 리졸브되어야 한다.
사용자는 ParameterResolver 인터페이스를 활용해 어떤 파라미터든 순서에 상관없이 주입할 수 있다.
현재 JUnit 5에는 3개의 리졸버가 기본으로 내장되어 있다.
TestInfoParameterResolver
TestInfoParameterResolver를 사용하면 테스트 클래스 생성자나 테스트 메서드에서 TestInfo 객체를 파라미터로 사용할 수 있다.
TestInfo는 현재 실행 중인 테스트나 컨테이너에 관한 정보를 제공하기 위해 사용한다.
즉, @Test
, @BeforeEach
, @AfterEach
, @BeforeAll
, @AfterAll
애노테이션이 달린 메서드에서 TestInfo 객체를 파라미터로 사용할 수 있다.
TestInfo는 디스플레이명, 테스트 클래스, 테스트 메서드, 관련 태그에 관한 정보 등 현재 테스트에 대한 정보를 가져온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class TestInfoTest {
TestInfoTest(TestInfo testInfo) {
assertEquals("TestInfoTest", testInfo.getDisplayName());
}
@BeforeEach
void setUp(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("display name of the method")
|| displayName.equals("testGetNameOfTheMethod(TestInfo)"));
}
@Test
void testGetNameOfTheMethod(TestInfo testInfo) {
assertEquals("testGetNameOfTheMethod(TestInfo)", testInfo.getDisplayName());
}
@Test
@DisplayName("display name of the method")
void testGetNameOfTheMethodWithDisplayNameAnnotation(TestInfo testInfo) {
assertEquals("display name of the method", testInfo.getDisplayName());
}
}
- 테스트 클래스 생성자
TestInfoTest
는 디스프레이명이 TestInfoTest인지 검사한다. @BeforeEach
애노테이션이 붙은 메서드는 각 테스트가 실행되기 전에 실행된다.- TestInfo를 가지고 디스플레이명이 예상한 이름인지 검증한다.
- 테스트 메서드에서도 TestInfo 타입의 파라미터를 사용했다.
- 각 테스트 메서드는 디스플레이명이 예상한 이름인지 검증한다.
- 첫 번째 테스트는 디스플레이명이 메서드명이다.
- 두 번째 테스트는 디스플레이명이
@DisplayName
애노테이션에 사용자 정의한 설명 글이다.
기본으로 내장되어 있는 TestInfoParameterResolver는 TestInfo 객체를 파라미터로 리졸브하는데,
TestInfo 객체는 현재의 컨테이너 또는 테스트에 대응한다.
즉, 생성자나 메서드에서 테스트에 관한 정보를 제공하는 데 사용한다.
TestReporterParameterResolver
TestReporterParameterResolver를 사용하면 테스트 클래스 생성자나 테스트 메서드에서 TestReporter 객체를 파라미터로 사용할 수 있다.
TestReporter는 함수형 인터페이스이므로 람다식이나 메서드 참조로 사용할 수 있다.
TestReporter는 한 개의 추상 메서드 publishEntry와 publishEntry 메서드를 오버로딩한 디폴트 메서드 여러 개를 가진다.
TestReporter 타입의 파라미터는 @BeforeEach
, @AfterEach
, @Test
애노테이션이 달린 테스트 메서드에 주입할 수 있다.
TestReporter 객체는 현재 실행되는 테스트에 추가적인 정보를 제공할 때 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestReporter;
import java.util.HashMap;
import java.util.Map;
public class TestReporterTest {
@Test
void testReportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("Single value");
}
@Test
void testReportKeyValuePair(TestReporter testReporter) {
testReporter.publishEntry("Key", "Value");
}
@Test
void testReportMultipleKeyValuePairs(TestReporter testReporter) {
Map<String, String> values = new HashMap<>();
values.put("user", "John");
values.put("password", "secret");
testReporter.publishEntry(values);
}
}
- 첫 번째 메서드(testReportSingleValue)
- 단일한 문자열 값을 출력하는 데 사용했다.
- 두 번째 메서드(testReportKeyValuePair)
- 키-값 쌍을 출력하는 데 사용했다.
- 세 번째 메서드(testReportMultipleKeyValuePairs)
- 맵을 생성하고, 맵에 두 개의 키-값 쌍을 넣은 다음, 생성된 맵을 출력하는 데 사용했다.
이렇게 기본으로 내장된 TestReporterParameterResolver는 테스트 리포트를 들 때 사용할 수 있는 TestReporter 객체를 파라미터로 리졸브한다.
RepetitionInfoParameterResolver
RepetitionInfoParameterResolver는 @RepeatedTest
, @BeforeEach
, @AfterEach
애노테이션이 달린 메서드의 파라미터가 RepetitionInfo 타입일 때 RepetitionInfo 인스턴스를 리졸브하는 역할을 한다.
RepetitionInfo는 @RepeatedTest
애노테이션이 달린 테스트에 대한 현재 반복 인덱스와 총 반복 횟수에 대한 정보를 가지고 있다.
반복 테스트는 아래에서 설명한다.
반복 테스트
JUnit 5에서는 @RepeatedTest
애노테이션을 사용하여 반복 횟수를 지정한 후 해당 횟수만큼 테스트를 반복할 수 있다.
반복 테스트는 테스트 실행 간에 사전 조건이 변경될 수 있는 환경에서 특히 유용하다.
@RepeatedTest
지원하는 플레이스홀더- {displayName} :
@RepeatedTest
애노테이션이 붙은 메서드의 디스플레이명 - {currentRepetition} : 현재 반복 인덱스
- {totalRepetitions} : 총 반복 횟수
- {displayName} :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.junit.jupiter.api.*;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class RepeatedTestsTest {
private static final Set<Integer> integerSet = new HashSet<>();
private static final List<Integer> integerList = new ArrayList<>();
@RepeatedTest(value = 5,
name = "{displayName} - repetition {currentRepetition}/{totalRepetitions}")
@DisplayName("Test add operation")
void addNumber() {
Calculator calculator = new Calculator();
assertEquals(2, calculator.add(1, 1), "1 + 1 should equal 2");
}
@RepeatedTest(value = 5,
name = "the list contains {currentRepetition} elements(s), the set contains 1 element")
void testAddingToCollections(TestReporter testReporter,
RepetitionInfo repetitionInfo) {
integerSet.add(1);
integerList.add(repetitionInfo.getCurrentRepetition());
testReporter.publishEntry("Repetition number",
String.valueOf(repetitionInfo.getCurrentRepetition()));
assertEquals(1, integerSet.size());
assertEquals(repetitionInfo.getCurrentRepetition(), integerList.size());
}
}
- 첫 번째 테스트(addNumber)
- 다섯 번 반복되었다.
- 각 반복마다 디스플레이명, 현재 반복 인덱스, 전체 반복 횟수를 보여 준다.
- 두 번째 테스트
- 다섯 번 반복되었다.
- 각 반복마다 리스트의 요소 수를 표시하고, 집합에 항상 하나의 요소만 있는지 검증한다.
- 테스트가 반복될 때마다
RepetitionInfo
가 현재 반복 인덱스를 가지고 있는 것을 알 수 있다.
파라미터를 사용한 테스트
파라미터를 사용한 테스트는 하나의 테스트를 다양한 파라미터를 가지고 여러 번 실행하게해 주는 기능이다.
파라미터를 사용한 테스트의 최대 강점은 다양항 입력을 두고 테스트를 실행할 수 있다는 것이다.
파라미터를 사용한 테스트에는 @ParameterizedTest
애노테이션이 달려 있다.
테스트에 파라미터를 사용하려면 반드시 각 반복에 대한 파라미터를 제공하는 소스를 선언해야 한다.
그러면 데이터가 파라미터로 테스트 메서드에 전달된다.
@ValueSource
@ValueSource
애노테이션을 사용하면 문자열 배열을 입력 값으로 지정할 수 있다.
각 테스트 수행마다 문자열 배열로부터 파라미터를 사용한 테스트에 순서대로 입력 데이터를 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ParameterizedWithValueSourceTest {
private final WordCounter wordCounter = new WordCounter();
@ParameterizedTest
@ValueSource(strings = { "Check three parameters", "JUnit in Action"})
void testWordsInSentence(String sentence) {
assertEquals(3, wordCounter.countWords(sentence));
}
}
@ParameterizedTest
애노테이션을 사용하여 해당 테스트가 파라미터를 사용한 테스트임을 명시한다.- 테스트 메서드의 파라미터로 전달할 값을 특정한다.
- 테스트 메서드는
@ValueSource
에 적혀 있는 문자열의 수만큼 총 2번 실행된다.
@EnumSource
@EnumSource
를 사용하면 파라미터에 enum을 사용할 수 있다.
names 속성응ㄹ 통해 @EnumSource
에서 사용하거나 제외할 열거형 인스턴스를 지정할 수 있다.
기본적으로는 열거형의 모든 인스턴스를 대상으로 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.params.provider.EnumSource.Mode.EXCLUDE;
public class ParameterizedWithEnumSourceTest {
private final WordCounter wordCounter = new WordCounter();
@ParameterizedTest
@EnumSource(Sentences.class)
void testWordsInSentence(Sentences sentences) {
assertEquals(3, wordCounter.countWords(sentences.value()));
}
@ParameterizedTest
@EnumSource(value = Sentences.class,
names = {"JUNIT_IN_ACTION", "THREE_PARAMETERS"})
void testSelectedWordsInSentence(Sentences sentences) {
assertEquals(3, wordCounter.countWords(sentences.value()));
}
@ParameterizedTest
@EnumSource(value = Sentences.class, mode = EXCLUDE,
names = {"THREE_PARAMETERS"})
void testExcludedWordsInSentence(Sentences sentences) {
assertEquals(3, wordCounter.countWords(sentences.value()));
}
enum Sentences {
JUNIT_IN_ACTION("JUnit in Action"),
SOME_PARAMETERS("Check some parameters"),
THREE_PARAMETERS("Check three parameters");
private final String sentence;
Sentences(String sentence) {
this.sentence = sentence;
}
public String value() {
return sentence;
}
}
}
- 첫 번째 테스트(testWordsInSentence)
@ParameterizedTest
애노테이션을 달아 파라미터를 사용한 테스트임을 명시한다.@EnumSource
의 대상을 Sentences.class 열거형 전체로 잡는다.- Sentences 열거형의 각 인스턴스에 대해 한 번씩 총 3번 실행된다.
- 두 번째 테스트(testSelectedWordsInSentence)
@ParameterizedTest
애노테이션을 달아 파라미터를 사용한 테스트임을 명시한다.@EnumSource
에서 Sentences.class를 사용하지만, “JUNIT_IN_ACTION”, “THREE_PARAMETERS”으로 한정한다.- 총 2번 실행된다.
- 세 번째 테스트(testExcludedWordsInSentence)
@ParameterizedTest
애노테이션을 달아 파라미터를 사용한 테스트임을 명시한다.@EnumSource
에서 Sentences.class를 사용하지만, “THREE_PARAMETERS”를 제외했다.- 총 2번 실행된다.
@CsvSource
@CsvSource
애노테이션을 사용하여 CSV 형식으로 파라미터를 제공할 수도 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ParameterizedWithCsvSourceTest {
private final WordCounter wordCounter = new WordCounter();
@ParameterizedTest
@CsvSource({"2, Unit testing", "3, JUnit in Action", "4, Write solid Java code"})
void testWordsInSentence(int expected, String sentence) {
assertEquals(expected, wordCounter.countWords(sentence));
}
}
@ParameterizedTest
애노테이션을 달아 파라미터를 사용한 테스트임을 명시한다.- 테스트에 전달된 파라미터는
@CsvSource
애노테이션에 나열된 CSV 형식의 문자열에서 구문을 분석하여 가져온다. - 그러므로 이 테스트는 각 CSV 행에 대해 한 번씩 총 3번 실행된다.
- 각 CSV 행은 구문 분석되어 첫 번째 값은 expected에 할당되고, 두 번째 값은 sentence에 할당된다.
@CsvFileSource
@CsvFileSource
애노테이션을 사용하면 클래스패스에 있는 CSV 파일을 파라미터의 소스로 사용할 수 있다.
이때도 마찬가지로 파라미터를 사용한 테스트는 CSV 파일의 각 행마다 한 번씩 실행된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class ParameterizedWithCsvFileSourceTest {
private final WordCounter wordCounter = new WordCounter();
@ParameterizedTest
@CsvFileSource(resources = "/word_counter.csv")
void testWordsInSentence(int expected, String sentence) {
assertEquals(expected, wordCounter.countWords(sentence));
}
}
위 코드에서 사용한 word_counter.csv 파일은 아래 내용과 같고, resources 경로에 있다.
2, Unit testing
3, JUnit in Action
4, Write solid Java code
@ParameterizedTest
애노테이션을 달아 파라미터를 사용한 테스트임을 명시한다.@CsvFileSource
애노테이션에 등록된 리소스를 파라미터로 사용한다.- CSV 파일의 행별로 한 번씩 총 3번 실행된다.
- CSV 파일의 각 행을 구문 분석하여 첫 번째 값은 expected에 할당하고, 두 번째 값은 sentence에 할당한다.
동적 테스트
JUnit 5는 런타임에 테스트를 생성할 수 있는 동적 프로그래밍 모델을 도입했다.
개발자가 팩터리 메서드를 작성하기만 하면 프레임워크가 런타임에 실행할 테스트를 생성한다.
팩터리 메서드에는 @TestFactory
애노테이션을 달면된다.
@TestFactory
메서드는 일반적인 테스트가 아니라 테스트를 생성하는 팩터리다.
@TestFactory
반환할 수 있는 대상- DynamicNode(추상 클래스, DynamicContainer나 DynamicTest가 DynamicNode를 상속했고, 인스턴스화가 가능한 구체 클래스)
- DynamicNode 객체의 배열
- DynamicNode 객체의 스트림
- DynamicNode 객체의 컬렉션
- DynamicNode 객체의 Iterable
- DynamicNode 객체의 반복자(iterator)
DynamicTest 인터페이스는 디스플레이명과 Executable로 이루어져 있고, 런타임에 생성되는 테스트다.
Executable은 자바 8에서 등장한 함수형 인터페이스이므로, 동적 테스트는 람다식이나 메서드 참조 방식으로 구현할 수 있다.
동적 테스트는 @Test
애노테이션이 달린 보통의 테스트와 다른 생애 주기를 가지고 있다.
@BeforeEach
, @AfterEach
애노테이션이 달린 메서드는 @TestFactory
메서드 전체에 대해 실행될 뿐 개별 테스트 각각에 대해서는 실행되지 않는다.
팩터리 메서드 외에 개별 동적 테스트에 대한 생애 주기 콜백은 없다.
@BeforeAll
, @AfterAll
애노테이션이 동작하는 방식도 동일하다.(전체 테스트를 시작하기 전과 끝낸 다음에 실행된다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import org.example.junitinaction3.chapter12.predicate.PositiveNumberPredicate;
import org.junit.jupiter.api.*;
import java.util.Iterator;
import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
public class DynamicTestsTest {
private final PositiveNumberPredicate predicate = new PositiveNumberPredicate();
@BeforeAll
static void setUpClass() {
System.out.println("@BeforeAll method");
}
@AfterAll
static void tearDownClass() {
System.out.println("@AfterAll method");
}
@BeforeEach
void setUp() {
System.out.println("@BeforeEach method");
}
@AfterEach
void tearDown() {
System.out.println("@AfterEach method");
}
@TestFactory
Iterator<DynamicTest> positiveNumberPredicateTestCases() {
return asList(
dynamicTest("negative number",
() -> assertFalse(predicate.check(-1))),
dynamicTest("zero",
() -> assertFalse(predicate.check(0))),
dynamicTest("positive number",
() -> assertTrue(predicate.check(1)))
).iterator();
}
}
@BeforeAll
,@AfterAll
애노테이션이 달린 setUpClass 메서드와 tearDownClass 메서드는 전체 테스트를 시작하기 전과 끝낸 다음에 한 번씩 실행된다.@BeforeEach
,@AfterEach
애노테이션이 달린 setUp 메서드와 tearDown 메서드는@TestFactory
애노테이션이 달린 메서드가 실행되기 전후에 실행된다.- 팩터리 메서드는 “negative number”, “zero”, “positive number” 레이블을 달고 있는 3가지 테스트 메서드를 생성한다.
- 각 테스트는 dynamicTest 메서드의 두 번째 파라미터로 주어지는 Executable 객체를 실행한다.
Hamcrest 매처 사용하기
더 많은 단위 테스트와 단언문을 작성하면서 개발자들은 몇몇 단언문이 다루는 내용이 너무 많고 한번에 읽기 어렵다는 것을 느끼게 된다.
아래 코드는 Hamcrest를 사용하지 않아 알아보기 힘든 단언문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
public class HamcrestListTest {
private List<String> values;
@BeforeEach
public void setUp() {
values = new ArrayList<>();
values.add("John");
values.add("Michael");
values.add("Edwin");
}
@Test
@DisplayName("Hamcrest를 사용하지 않은 테스트")
public void testWithoutHamcrest() {
assertEquals(3, values.size());
assertTrue(values.contains("Oliver")
|| values.contains("Jack")
|| values.contains("Harry"));
}
}
...
@BeforeEach
가 달린 setUp 메서드는 테스트 데이터를 초기화하는 픽스처 역할을 한다.- 테스트 메서드를 보면 간단하지만, 한 눈에는 읽기 힘들다.
이 문제를 해결하기 위해 Hamcrest 매처 라이브러리를 사용하여 테스트 표현식을 작성해보자.
매처는 제약 또는 술어라고도 한다.
아래 코드는 Hamcrest를 사용해 간명하게 만든 단언문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
...
public class HamcrestListTest {
...
@Test
@DisplayName("Hamcrest를 사용해서 자세한 실패 정보를 나타내는 테스트")
public void testListWithoutHamcrest() {
assertEquals(values, hasSize(3));
assertTrue(values, hasItem(anyOf(
equalsTo("Oliver"),
equalsTo("Jack"),
equalsTo("Harry"))));
}
}
...
- 매처를 중첩해서 사용했는데, 이는 매처의 가장 강력한 기능 중 하나다.
Hamcrest는 단언문이 실패했을 때 실패한 내용에 대해 기본 단언문보다 조금 더 읽기 쉽게 설명한다.
아래는 테스트 실행 결과이다.
Hamcrest를 사용하지 않은 테스트 실행 결과
두 그림을 비교하면 알 수 있듯 Hamcrest 매처를 사용한 테스트가 훨씬 더 자세한 정보를 제공한다.
이제 Hamcrest 매처를 사용해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import org.example.junitinaction3.chapter02.hamcrest.Customer;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.hamcrest.CoreMatchers.anyOf;
import static org.hamcrest.CoreMatchers.allOf;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
import static org.hamcrest.Matchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
public class HamcrestMatchersTest {
private static final String FIRST_NAME = "John";
private static final String LAST_NAME = "Smith";
private static final Customer customer = new Customer(FIRST_NAME, LAST_NAME);
@Test
@DisplayName("Hamcrest is, anyOf, allOf 매처를 사용한 테스트")
public void testHamcrestIs() {
int price1 = 1, price2 = 1, price3 = 2;
assertThat(1, is(price1));
assertThat(1, anyOf(is(price2), is(price3)));
assertThat(1, allOf(is(price1), is(price2)));
}
@Test
@DisplayName("nullValue 매처를 사용한 테스트")
void testNull() {
assertThat(null, nullValue());
}
@Test
@DisplayName("notNullValue 매처를 사용한 테스트")
void testNotNull() {
assertThat(customer, notNullValue());
}
@Test
@DisplayName("hasProperty 매처를 사용한 테스트")
void checkCorrectCustomerProperties() {
assertThat(customer, allOf(
hasProperty("firstName", is(FIRST_NAME)),
hasProperty("lastName", is(LAST_NAME))
));
}
}
- 첫 번째 테스트(testHamcrestIs)
- is, anyOf, allOf를 사용했다.
- anyOf, allOf에는 is를 여러 번 중첩했다.
is : 문장의 가독성을 높이고 싶을 때 사용한다.(일종의 장식 표현이다.)
allOf : 모든 매처 규칙을 만족하는지 확인한다.(&& 연산자와 비슷하다.)
anyOf : 하나라도 일치하는 매치 규칙이 있는지 확인한다.(|| 연산자와 비슷하다.)
- 두 번째 테스트(testNull)
- nullValue 매처를 사용하여 예상 값이 null인지 검증한다.
nullValue : null인지 확인한다.
- nullValue 매처를 사용하여 예상 값이 null인지 검증한다.
- 세 번째 테스트(testNotNull)
- notNullValue 매처를 사용하여 customer가 null이 아닌지 검증한다.
notNullValue : null이 아닌지 확인한다.
- notNullValue 매처를 사용하여 customer가 null이 아닌지 검증한다.
- 네 번째 테스트(checkCorrectCustomerProperties)
- hasProperty 메서드를 사용하여 customer 변수에 대해 firstName과 lastName 속성이 각각 FIRST_NAME과 LAST_NAME인지 검증한다.
- allOf 매처를 사용했으므로 매치 규칙을 모두 만족해야 한다.
hasProperty : 객체가 특정 속성을 가졌는지 확인한다.
Hamcrest는 확장성이 매우 뛰어나서 특정 규칙을 확인하는 매처를 작성하기가 쉽다.
Matcher 인터페이스와 적절하게 명명된 팩터리 메서드를 사용하면 된다.
정리
인상깊었거나 중요하다고 생각한 부분은 아래와 같다.
- JUnit은 테스트 메서드의 격리성을 보장한다.
- 단언문에서 람다식을 파라미터로 사용할 때의 이점은 지연 전달 덕분에 성능이 향상된다는 데 있다.
- assertThrows 메서드를 사용하면 시스템에서 할당된 Job 객체가 없을 때 예외를 던지는지 확인할 수 있으므로 테스트 가독성이 좋아진다.
아직 Hamcrest는 유용할지 모르겠어서 사용해봐야 알 것 같다.