JUnit Jupiter
JUnit Vintage
JUnit Platform
스프링 부트 2.2버전 이상 부터는 기본적으로 JUnit5 의존성 추가된다.
스프링 부트 프로젝트가 아닐 경우에는
junit-jupiter-api를 테스트 구현으로 사용하고, JUnit Platform을 이용하여 테스트를 실행하도록 설정
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.8.2")
}
test{
useJUnitPlatform {
includeTags("fast", "smoke & feature-a")
// excludeTags("slow", "ci")
includeEngines("junit-jupiter")
// excludeEngines("junit-vintage")
}
testLogging {
events "passed", "skipped", "failed"
}
}
Build.gradle dependencies에 spring-boot-starter-test 추가(기본적으로 추가 되어있음)
dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
spring-boot-starter-test에 존재하는 라이브러리들
@Test -> 테스트용 메소드를 표현
@BeforeEach -> 각 테스트 메소드가 시작되기 전, 실행되어야하는 메소드 표현
@AfterEach -> 각 테스트 메소드가 시작된 후, 실행되어야 하는 메소드 표현
@BeforeAll -> 테스트 시작 전에 실행되어야 하는 메소드를 표현
@AfterAll -> 테스트 종료 후에 실행되어야 하는 메소드를 표현
@BeforeAll -> @BeforeEach -> @Test -> @AfterEach -> @BeforeEach
-> @Test -> @AfterEach -> @AfterAll
// @DiplayName -> 테스트의 이름을 지정 가능, 공백, Emoji, 특수문자 등을 모두 지원
public class DisplayNameTest {
@Nested
@DisplayName("A 테스트")
class testA {
@Test
@DisplayName("성공")
public void success() { /* */ }
@Test
@DisplayName("실패")
public void fail() { /* */ }
}
@Nested
@DisplayName("숫자")
class testNumber {
@Nested
@DisplayName("1 테스트")
class test1 {
@Test
@DisplayName("성공")
public void success() { /* */ }
@Test
@DisplayName("실패")
public void fail() { /* */ }
}
@Nested
@DisplayName("2 테스트")
class test2 {
@Test
@DisplayName("성공")
public void success() { /* */ }
@Test
@DisplayName("실패")
public void fail() { /* */ }
}
}
}
// 물론 함수명 자체를 한글로 작성해서 사용할 수 도 있다.
@Test
public void 성공() { /* */ }
@Test
public void 실패() { /* */ }
// @Nested
// -> 테스트 클래스 안에서 내부 클래스를 정의해서 테스트를 계층화 할 때 사용
// -> 내부 클래스는 부모클래스의 멤버 필드에 접근 가능
// -> Before / After 와 같은 테스트 생명주기에 관계된 메소드들도 계층에 맞춰 동작
package com.johngrib.example;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.assertTrue;
@SuppressWarnings({"InnerClassMayBeStatic", "NonAsciiCharacters"})
@DisplayName("ComplexNumber 클래스")
class ComplexNumberKoTest {
@Nested
@DisplayName("toString 메소드는")
class Describe_toString {
@Nested
@DisplayName("만약 실수값만 있고 허수값이 없다면")
class Context_with_real {
private final double givenNatual = 3d;
private final String expectPattern = "^3(?:\\.0+)?$";
private ComplexNumber given = ComplexNumber.of(givenNatual);
@Test
@DisplayName("실수부만 표현한 문자열을 리턴한다")
void it_returns_a_valid_string() {
Assertions.assertTrue(given.toString().matches(expectPattern));
}
}
@Nested
@DisplayName("만약 실수값이 있고 허수값도 있다면")
class Context_with_real_and_imagine {
private final double givenNatual = 3d;
private final double givenImagine = 7d;
private ComplexNumber given = ComplexNumber.of(givenNatual, givenImagine);
private String expectPattern = "^3(?:\\.0+)?\\+7(?:\\.0+)?i$";
@Test
@DisplayName("실수부 + 허수부i 형식으로 표현한 문자열을 리턴한다")
void it_returns_a_valid_string() {
assertTrue(given.toString().matches(expectPattern));
}
}
@Nested
@DisplayName("sum 메소드는")
class Describe_sum {
@Nested
@DisplayName("만약 실수부와 허수부가 있는 두 복소수가 주어진다면")
class Context_with_two_complex {
private ComplexNumber a, b;
@BeforeEach
void prepareNumbers() {
a = ComplexNumber.of(1d, 2d);
b = ComplexNumber.of(32d, 175d);
}
ComplexNumber subject() {
return ComplexNumber.sum(a, b);
}
@Test
@DisplayName("실수부와 허수부가 올바르게 계산된 복소수를 리턴한다")
void it_returns_a_valid_complex() {
Assertions.assertEquals(a.getReal() + b.getReal(), subject().getReal(),
"리턴된 복소수는 두 실수 값의 합을 실수로 갖는다");
Assertions.assertEquals(a.getImagine() + b.getImagine(), subject().getImagine(),
"리턴된 복소수는 두 허수 값의 합을 허수로 갖는다");
}
}
}
@Nested
@DisplayName("of 메소드는")
class Describe_of {
private final double givenReal = 3d;
private final double givenImagine = 3d;
@Nested
@DisplayName("만약 실수값만 주어지고 허수값은 없다면")
class Context_with_real {
@Test
@DisplayName("i 값이 0 인 복소수를 리턴한다")
void it_returns_a_valid_complex() {
final ComplexNumber result = ComplexNumber.of(givenReal);
Assertions.assertEquals(result.getImagine(), 0d, "리턴된 복소수는 허수 값으로 0 을 갖는다");
Assertions.assertEquals(result.getReal(), givenReal, "리턴된 복소수는 실수 값으로 주어진 실수 값을 갖는다");
}
}
@Nested
@DisplayName("만약 실수값과 허수값이 주어진다면")
class Context_with_real_and_i {
@Test
@DisplayName("주어진 실수값과 허수값을 갖는 복소수를 리턴한다")
void it_returns_a_valid_complex() {
final ComplexNumber result = ComplexNumber.of(givenReal, givenImagine);
Assertions.assertEquals(result.getReal(), givenReal, "리턴된 복소수는 실수 값으로 주어진 실수 값을 갖는다");
Assertions.assertEquals(result.getImagine(), givenImagine, "리턴된 복소수는 허수 값으로 주어진 허수 값을 갖는다");
}
}
}
}
// @Disabled -> 특정 테스트를 실행하지 않음
public class DisplayNameTest {
@Disabled
@Nested
@DisplayName("A 테스트")
class testA {
@Test
public void success() { /* */ }
@Test
@DisplayName("실패")
public void fail() { /* */ }
}
@Nested
@DisplayName("숫자")
class testNumber {
@Nested
@DisplayName("1 테스트")
class test1 {
@Test
@DisplayName("성공")
public void success() { /* */ }
@Test
@DisplayName("실패")
public void fail() { /* */ }
}
@Nested
@DisplayName("2 테스트")
class test2 {
@Test
@DisplayName("성공")
public void success() { /* */ }
@Test
@DisplayName("실패")
public void fail() { /* */ }
}
}
}
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() -> 테스트를 실패 처리한다.
assertTimeout(duration, executable) -> 특정 시간 안에 실행이 완료되는지 확인
ex) assertTimeout(ofMinutes(2), () -> Thread.sleep(10));
assertThrows(Class<T> expectedType, Executable executable)
-> executable을 실행한 결과로 지정한 타입의 익셉션이 발생하는지 검사한다.
ex) assertThrows(IllegalArgumentException.class,
() -> {
AuthService authService = new AuthService();
authService.authenticate(null, null);
});
assertDoesNotThrow(Executable executable) -> executable을 실행한 결과로 익셉션이 발생하지 않는지 검사한다.
import static org.assertj.core.api.Assertions.assertThat;
@Test
void a_few_simple_assertions() {
assertThat("The Lord of the Rings").isNotNull()
.startsWith("The")
.contains("Lord")
.endsWith("Rings");
}
@Test
void filter_test2() {
List<Human> list = new ArrayList<>();
Human kim = new Human("Kim", 22);
Human park = new Human("Park", 25);
Human lee = new Human("Lee", 25);
Human amy = new Human("Amy", 22);
Human jack = new Human("Jack", 22);
list.add(kim);
list.add(park);
list.add(lee);
list.add(amy);
list.add(jack);
assertThat(list).filteredOn("age", 25).containsOnly(park, lee);
}
@SpringBootTest
-> 통합 테스트 용도로 사용됨 @SpringBootApplication을 찾아가 하위의 모든 Bean을 스캔하여 로드함
그 후 Test용 Application Context를 만들어 Bean을 추가하고, MockBean을 찾아 교체
@Transactional
-> 기본적으로 Transactional을 테스트에서 사용하면, 테스트가 끝날 경우 강제로 롤백이 된다.
-> 롤백을 시키고 싶지 않을 경우에는 Rollback(false)를 추가하면 된다.
ex)
@Test
@Transactional
@Rollback(false)
======================================================================
@ExtendWith
-> JUnit4에서 @RunWith로 사용되던 어노테이션이 ExtendWith로 변경됨
@ExtendWith는 메인으로 실행될 Class를 지정할 수 있음
ex)
@ExtendWith(MockitoExtension.class)
class DataServiceTests {
@InjectMocks
private DataService service;
@Mock
private WpasDataMapper mapper;
@BeforeEach
public void setUp() {
System.out.println("setUp");
mockMapper();
service.initQcStepItem();
}
@Test
void test_InspectQcData() {
service.inspectQcData();
}
}
=====================================================================
@WebMvcTest(Class명.class)
-> @Controller, @RestController가 설정된 클래스들을 찾아 메모리에 생성한다.
-> @Service나 @Repository가 붙은 객체들은 테스트 대상이 아닌 것으로 처리되기 때문에 생성되지 않는다.
-> @WebMvcTest가 설정된 테스트 케이스에서는 서블릿 컨테이너를 모킹한 MockMvc타입의 객체를 목업하여 컨트롤러에 대한 테스트코드를 작성할 수 있다.
-> @WebMvcTest 어노테이션을 사용하면 MVC 관련 설정인
@Controller, @ControllerAdvice, @JsonComponent와 Filter, WebMvcConfigurer,
HandlerMethodArgumentResolver만 로드되기 때문에,
실제 구동되는 애플리케이션과 똑같이 컨텍스트를 로드하는
@SpringBootTest 어노테이션보다 가볍게 테스트 할 수 있다.
@AutoConfigureMockMvc
-> spring.test.mockmvc의 설정을 로드하면서 MockMvc의 의존성을 자동으로 주입
-> MockMvc 클래스는 REST API 테스트를 할 수 있는 클래스
MockMvc
-> Controller의 API를 테스트하는 용도인 MockMvc 객체를 주입 받음
Perform()메소드를 활용하여 컨트롤러의 동작을 확인할 수있음
andExpect(), andDo(), andReturn() 등의 메소드를 같이 활용함
ex)
andExpect() -> 응답 결과를 검증할 수 있는 메서드
status() 메소드 -> isOK(), isNotFound(), isMethodNotAllowed(), isInternalServerError(),
is(int status)
view()
-> 컨트롤러가 리턴하는 뷰를 검증할 때는 view() 메소드를 사용
-> andExpect(view().name("hello"))
model()
-> attributeExists(String name) = name에 해당하는 데이터가 Model에 포함되어있는지 검증한다.
-> attribute(String name, Object value) = name에 해당하는 데이터가 value 객체인지 검증한다.
@ExtendWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class MockTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testHelloWith둘리() throws Exception {
mockMvc.perform(get("/hello").param("name", "둘리"))
.andExpect(status().isOK())
.andExpect(content().string("Hello : 둘리"))
.andDo(print());
}
}
======================================================================
@MockBean
-> 기존에 사용되던 Bean이 아닌 MockBean을 주입한다.
-> Bean의 어떤 메소드/어떤 값이 입력 되면 어떤 값이 리턴 되어야 한다는 내용을
개발자 필요에 의해서 조작이 가능
ex)
@MockBean(name="httpSession")
private HttpSession httpSession;
@Test
public void Test() {
Customer customre = new Customer();
given(httpSession.getAttribute("loginUser").willReturn(customer));
...
}
@RunWith(SpringRunner.class)
@WebMvcTest({UserController.class, JwtAuthenticationInterceptor.class})
public class UserControllerTests {
@MockBean
private UserService userService;
@Autowired
MockMvc mockMvc;
@Test
public void findUserById_테스트() throws Exception {
UserDto userDto = new UserDto();
userDto.setUsername("sa1341");
userDto.setPassword("wnsdud2");
UserResponseDto responseDto = new UserResponseDto();
responseDto.setId(1);
responseDto.setUsername("sa1341");
//given
given(userService.findUser(any())).willReturn(responseDto);
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(userDto);
//when
ResultActions resultActions = mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(json))
.andDo(print());
//then
resultActions
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8_VALUE))
.andExpect(jsonPath("$.username", is("sa1341")))
.andDo(print());
}
}
Unit Test testing 방법
테스트시 결과는 아래와 같이 전체 확인이 가능하다.
참조
https://junit.org/junit5/docs/current/user-guide/#extensions-exception-handling