JUnit은 자바 언어를 위한 유닛 테스트 프레임워크로, 자바 애플리케이션에서 테스트 코드를 작성하고 실행하는 데 사용된다. 코드의 작은 단위인 함수와 메소드 별로 테스트케이스를 작성하는 단위테스트에 사용되며, 내가 의도한 대로 동작하는지 단정문(Assert)으로 기대값에 대한 수행결과를 확인 할 수 있다.
Spring Boot 2.2 버전부터는 JUnit 5 버전을 사용하는데 JUnit 5는 JUnit Platform, Jupiter, Vintage 세 가지 주요 모듈로 구성되어있다.
Annotation | Description |
---|---|
@Test | 테스트용 메소드를 표현하는 어노테이션 |
@BeforeEach | 각 테스트 메소드가 시작되기 전에 실행되어야 하는 메소드를 표현 |
@AfterEach | 각 테스트 메소드가 시작된 후 실행되어야 하는 메소드를 표현 |
@BeforeAll | 테스트 시작전에 시작되어야 하는 메소드를 표현 (static 처리 필요) |
@AfterAll | 테스트 종료후에 시작되어야 하는 메소드를 표현 (static 처리 필요) |
Annotation | Description |
---|---|
@SpringBootTest | 통합테스트 용도로 사용됨 |
@SpringBootApplication을 찾아가 하위의 모든 Bean을 스캔하여 로드함 | |
그 후 Test용 Application Context를 만들어 Bean을 추가하고, MockBean을 찾아 교체 | |
@ExtendWith | 테스트 실행을 확장하기 위해 사용됨 |
JUnit 4에서 @RunWith 로 사용되던 애너테이션이 ExtendWith로 변경됨 | |
@WebMvcTest | 컨트롤러와 연관된 Bean이 모두 로드됨 |
@SpringBootTest대신 컨트롤러 관련 코드만 테스트 할 경우 사용 | |
@Autowired about Mockbean | Controller의 API를 테스트하는 용도인 MockMvc 객체를 주입 받음 |
perform() 메소드를 활용하여 컨트롤러의 동작을 확인할 수 있음 | |
.andExpect(),andDo(), andReturn()등의 메소드를 같이 활용함 | |
@MockBean | 의존성이 있는 경우 가짜 객체를 생성해주는 어노테이션 |
given() 메소드를 활용하여 가짜객체의 동작에 대해 정의하여 사용할 수 있음 | |
@AutoConfigureMockMvc | MockMvc의 의존성을 자동으로 주입 |
@Import | 필요한 class 들을 Configuration으로 만들어서 사용할 수 있음 |
단위테스트는 단위가 최대한 작아야 하고 빠르게 실행되고 빠르게 결과를 알아야 한다. 또한 실제 서버나 데이터베이스를 거치지 않고 가짜 데이터를 만들어서 테스트를 진행한다. 단위테스트는 독립적이여야 하고 다른테스트에 의존하거나 영향을 주어선 안된다. 그리고 반복 가능해야하고 몇번을 진행하든 똑같은 결과가 나와야 한다. 결과는 성공이나 실패로 분명하게 알 수 있어야하고 TDD원칙에 따라 시기적절한 때에 작성되어야 한다.
원칙 | 내용 |
---|---|
Fast | 테스트 코드의 실행은 빠르게 진행되어야 함 |
Independent | 독립적인 테스트가 가능해야 함 |
Repeatable | 테스트는 반복 가능하고 매번 같은 결과를 만들어야 함 |
Self-Validation | 테스트는 그 자체로 실행하여 결과를 검증할 수 있어야 함 |
Timely | 단위테스트는 비즈니스 코드가 완성되기 전에 구성하고 테스트가 가능해야 함 |
Assertion은 ‘예상하는 결과 값이 참(true)이길 바라는 논리적인 표현’이다.
JUnit에서는 Assertion과 관련된 다양한 메서드를 사용해서 테스트 대상에 대한 Assertion을 진행할 수 있다.
- assertEquals()
: 기대하는 값과 실제 결과 값이 같은지를 검증할 수 있다
: 기대하는 문자열(expected)과 실제 결과 값(autual)이 일치하는지를 검증
- assertNotNull()
: 테스트 대상 객체가 null 이 아닌지를 테스트 한다.
: Null 여부 테스트
- assertThrows()
: 호출한 메서드의 동작 과정 중에 예외가 발생하는지 테스트
: 예외(Exception) 테스트
import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.concurrent.CountDownLatch;
import example.domain.Person;
import example.util.Calculator;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
class AssertionsDemo {
private final Calculator calculator = new Calculator();
private final Person person = new Person("Jane", "Doe");
@Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertEquals(4, calculator.multiply(2, 2),
"The optional failure message is now the last parameter");
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and all
// failures will be reported together.
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);
// Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}
@Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
}
@Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
new CountDownLatch(1).await();
});
}
private static String greeting() {
return "Hello, World!";
}
}
Assumption은 ‘~라고 가정하고’라는 표현을 쓸 때의 ‘가정’에 해당한다.
JUnit 5의 Assumption 기능을 사용하면 특정 환경에만 테스트 케이스가 실행되도록 할 수 있다.
- assumeTrue()
: 파라미터로 입력된 값이 true이면 나머지 아래 로직들을 실행한다.
- assumingThat()
: 첫 번째 인자의 값이 true이면, 두 번째 인자로 받은 검증을 수행한다.
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class AssumptionsDemo {
private final Calculator calculator = new Calculator();
@Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
}
@Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// remainder of test
}
@Test
void testInAllEnvironments() {
assumingThat("CI".equals(System.getenv("ENV")),
() -> {
// perform these assertions only on the CI server
assertEquals(2, calculator.divide(4, 2));
});
// perform these assertions in all environments
assertEquals(42, calculator.multiply(6, 7));
}
}