Mock environment
- MOCK 환경에서는 서버를 실행하지 않기 때문에 MockMvc나 WebTestClient로 테스트 해야 한다.
@SpringBootTest
@AutoConfigureMockMvc
class MyMockMvcTests {
@Test
void testWithMockMvc(@Autowired MockMvc mvc) throws Exception {
mvc.perform(get("/")).andExpect(status().isOk()).andExpect(content().string("Hello World"));
}
@Test
void testWithWebTestClient(@Autowired WebTestClient webClient) {
webClient
.get().uri("/")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello World");
}
}
Student 시스템 통합 테스트
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nhnacademy.edu.springboot.student.Student;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.equalTo;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StudentControllerTest {
}
- MockMvc를 주입받아 /students를 호출한다.
- jsonPath를 이용하여 json 경로상의 값을 비교한다.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StudentControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
@Order(1)
void testGetStudents() throws Exception{
mockMvc.perform(get("/students"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].name", equalTo("manty")));
}
@Test
@Order(2)
void testGetStudent() throws Exception{
mockMvc.perform(get("/students/{id}", 1L))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.name", equalTo("manty")));
}
@Test
@Order(3)
void testCreateStudent() throws Exception{
ObjectMapper objectMapper = new ObjectMapper();
Student zbum = new Student(3L, "zbum1", 100);
mockMvc.perform(post("/students")
.content(objectMapper.writeValueAsString(zbum))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.name", equalTo("zbum1")));
}
@Test
@Order(4)
void deleteStudent() throws Exception{
this.mockMvc.perform(delete("/students/{id}", 3L))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.result", equalTo("OK")));
}
}
RANDOM_PORT, DEFINED_PORT environment
- WebFlux에서 서버를 실행하는 환경에서는 테스트하려면 WebTestClient를 사용해야 한다.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortWebTestClientTests {
@Test
void exampleTest(@Autowired WebTestClient webClient) {
webClient
.get().uri("/")
.exchange()
.expectStatus().isOk()
.expectBody(String.class).isEqualTo("Hello World");
}
}
- WebFlux를 사용할 수 없는 환경에서는 TestRestTemplate을 사용할 수 있다.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class MyRandomPortTestRestTemplateTests {
@Test
void exampleTest(@Autowired TestRestTemplate restTemplate) {
String body = restTemplate.getForObject("/", String.class);
assertThat(body).isEqualTo("Hello World");
}
}
@SpringBootTest(RANDOM_PORT)
import com.nhnacademy.edu.springboot.student.Student;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.*;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StudentControllerTest {
@Autowired
private TestRestTemplate testRestTemplate;
@Test
@Order(1)
void testGetStudents() throws Exception {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
HttpEntity<Student> entity = new HttpEntity<>(headers);
ResponseEntity<List<Student>> exchange = testRestTemplate.exchange(
"/students",
HttpMethod.GET,
entity,
new ParameterizedTypeReference<List<Student>>() {
});
assertThat(exchange.getBody())
.contains(new Student(1L, "manty", 100));
}
@Test
@Order(2)
void testGetStudent() throws Exception{
ResponseEntity<Student> result = testRestTemplate.getForEntity(
"/students/{id}",
Student.class,
1L);
assertThat(result.getBody())
.isEqualTo(new Student(1L, "manty", 100));
}
@Test
@Order(3)
void testCreateStudent() throws Exception{
Student zbum = new Student(3L, "zbum1", 100);
ResponseEntity<Student> result = testRestTemplate.postForEntity(
"/students",
zbum,
Student.class);
assertThat(result.getBody())
.isEqualTo(zbum);
}
@Test
@Order(4)
void testDeleteStudent() throws Exception{
testRestTemplate.delete(
"/students/{id}",
3L);
}
}
단위 테스트 - Mocking Beans
- 테스트 환경에서 사용할 수 없는 리모트 서비스등을 시뮬레이션 하도록 특정 컴포넌트를 Mocking
- @MockBean으로 빈을 생성하거나 빈을 대체할 수 있다.
@SpringBootTest
class MyTests {
@Autowired
private Reverser reverser;
@MockBean
private RemoteService remoteService;
@Test
void exampleTest() {
given(this.remoteService.getValue()).willReturn("spring");
String reverse = this.reverser.getReverseValue();
assertThat(reverse).isEqualTo("gnirps");
}
}
Student 시스템 - @MockBean
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
class StudentControllerMockBeanTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private StudentRepository studentRepository;
@Test
void testGetStudents() throws Exception{
given(studentRepository.findAll()).willReturn(List.of(new Student(100L, "AA", 90)));
mockMvc.perform(get("/students"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].name", equalTo("AA")));
}
}
Spying Beans
- 테스트 환경에서 이미 존재하는 빈을 래핑하여 특정 메서드가 다른 동작을 하도록 설정할 수 있다.
class MyTests {
@SpyBean
private RemoteService remoteService;
@Autowired
private Reverser reverser;
@Test
public void exampleTest() {
given(this.remoteService.someCall()).willReturn("mock");
String reverse = reverser.reverseSomeCall();
assertThat(reverse).isEqualTo("kcom");
then(this.remoteService).should(times(1)).someCall();
}
}
Student 시스템 @SpyBean
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class StudentControllerSpyBeanTest {
@Autowired
private MockMvc mockMvc;
@SpyBean
private StudentService studentService;
@Test
@Order(1)
void testGetStudents() throws Exception {
given(studentService.getStudents())
.will(invocation ->{
System.out.println("Spy!!");
return List.of(new Student(100L, "AA", 90));
});
mockMvc.perform(get("/students"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$[0].name", equalTo("AA")));
}
}
- 자동설정 환경에서 객체의 JSON 직렬화, 역직렬화를 테스트하기 위해 @JsonTest을 사용한다.
- AssertJ 기반의 테스트 지원을 제공하기 때문에 객체-JSON 매핑의 결과를 검증할 수 있다.
- JacksonTester, GsonTester, JsonbTester, BasicJsonTester 클래스를 각각의 JSON 라이브러리 헬퍼로 사용할 수 있다.
@JsonTest
class MyJsonTests {
@Autowired
private JacksonTester<VehicleDetails> json;
@Test
void serialize() throws Exception {
VehicleDetails details = new VehicleDetails("Honda", "Civic");
assertThat(this.json.write(details)).isEqualToJson("expected.json");
assertThat(this.json.write(details)).hasJsonPathStringValue("@.make");
assertThat(this.json.write(details)).extractingJsonPathStringValue("@.make")
.isEqualTo("Honda");
}
@Test
void deserialize() throws Exception {
String content = "{\"make\":\"Ford\",\"model\":\"Focus\"}";
assertThat(this.json.parse(content)).isEqualTo(new VehicleDetails("Ford", "Focus"));
assertThat(this.json.parseObject(content).getMake()).isEqualTo("Ford");
}
}
- Spring MVC의 Controller를 테스트 하기 위해서는 @WebMvcTest를 사용한다.
- @WebMvcTest를 사용하면 @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations, HandlerMethodArgumentResolver 등만 스캔한다.
- 테스트에서 다른 컴포넌트를 스캔하고 싶다면 테스트코드에 @Import로 직접 설정해 주어야 한다.
- @WebMvcTest를 사용하면 MockMvc 객체를 얻을 수 있다.
@WebMvcTest(UserVehicleController.class)
class MyControllerTests {
@Autowired
private MockMvc mvc;
@MockBean
private UserVehicleService userVehicleService;
@Test
void testExample() throws Exception {
given(this.userVehicleService.getVehicleDetails("sboot"))
.willReturn(new VehicleDetails("Honda", "Civic"));
this.mvc.perform(get("/sboot/vehicle").accept(MediaType.TEXT_PLAIN))
.andExpect(status().isOk())
.andExpect(content().string("Honda Civic"));
}
}
- HtmlUnit과 Selenium을 사용한다면 HtmlUnit의 WebClient도 사용할 수 있다.
@WebMvcTest(UserVehicleController.class)
class MyHtmlUnitTests {
@Autowired
private WebClient webClient;
@Test
void testExample() throws Exception {
HtmlPage page = this.webClient.getPage("/test");
assertThat(page.getBody().getTextContent()).isEqualTo("Honda Civic");
}
}
@WebMvcTest
@WebMvcTest(SystemController.class)
class SystemControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
SystemProperties systemProperties;
@Test
void testGetAuthor() throws Exception {
given(systemProperties.getAuthor())
.willReturn("ABCDEFG");
mockMvc.perform(get("/system/author"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.author", equalTo("ABCDEFG")));
}
}
- JPA를 테슨트 하기 위해서는 @DataJpaTest를 사용한다.
- @DataJpaTest를 사용하면 @Entity, Repository만 스캔한다.
- 기본적으로 테스트 이후에 수정된 정보는 모두 롤백한다.
- H2와 같은 인메모리 데이터베이스가 클래스 패스에 존재하면 사용하지만 실제 데이터베이스에서 테스트하려면 @AutoConfigureTestDatabase(replace=Replace.NONE)을 설정해야 한다.
- @DataJpaTest를 사용하면 TestEntityManager 객체를 얻을 수 있다.
@DataJpaTest
class MyRepositoryTests {
@Autowired
private TestEntityManager entityManager;
@Autowired
private UserRepository repository;
@Test
void testExample() throws Exception {
this.entityManager.persist(new User("sboot", "1234"));
User user = this.repository.findByUsername("sboot");
assertThat(user.getUsername()).isEqualTo("sboot");
assertThat(user.getEmployeeNumber()).isEqualTo("1234");
}
}
@DataJpaTest
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class StudentRepositorySliceTest {
@Autowired
TestEntityManager entityManager;
@Autowired
StudentRepository studentRepository;
@Test
void testFindAll() {
Student manty = new Student(10L, "Manty", 100);
entityManager.merge(manty);
Student student = studentRepository.findById(10L).orElse(null);
assertThat(student).isEqualTo(manty);
}
}
Test Utilities
- ConfigDataApplicationContextInitializer
- application.properties를 읽어들이는데 사용한다.
- @SpringBootTest가 제공하는 모든 기능이 필요 없을 때 사용한다.
- ConfigDataApplicationContextInitializer를 단독으로 사용하면 application.properties 내용을 스프링 Environment에 로드하는 것만 수행한다.
@ContextConfiguration(classes = Config.class, initializers = ConfigDataApplicationContextInitializer.class)
class MyConfigFileTests {
}
OutputCapture
- System.out, System.err으로 출력하는 내용을 잡아낼 수 있다.
- 수정할 수 없는 라이브러리의 결과를 확인할 때 사용할 수 있다.
@ExtendWith(OutputCaptureExtension.class)
class MyOutputCaptureTests {
@Test
void testName(CapturedOutput output) {
System.out.println("Hello World!");
assertThat(output).contains("World");
}
}
Custom Spring Boot Starter
Spring Boot Starter의 구성
자동설정 모듈
- 기능을 사용하기 위한 자동설정(auto-configure) 코드와 확장을 위한 설정키의 집합
Starter 모듈
- 필요한 라이브러리 집합을 제공하기 위한 starter
- 자동설정 모듈에는 라이브러리를 바로 사용할 수 있는 자동설정과 설정키의 정의,
- 콜백 인터페이스를 포함한다.
- Spring Boot의 annotation processor를 사용하여 메타데이터 파일을 생성할 수 있다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
Starter 모듈
- starter 모듈에는 java 코드를 포함하지 않는다.
- 라이브러리 의존성만을 제공하도록 구현한다.
Starter 명명법
- 적절한 네임스페이스를 제공해야 한다.
- 직접 작성하는 Starter에 spring-boot라는 네임스페이스를 사용하지 말것
- ${기능이름}-spring-boot-starter의 형식을 권장함
- MyBatis-Spring-Boot-Starter
설정키의 구성
- 설정키를 제공하고자 한다면, 유일한 네임스페이스를 사용해야 한다.
- Spring Boot가 사용하는 네임스페이스를 사용하지 말것 (server, management, spring 등)
- 가능하면 고유명사를 네임스페이스로 사용할 것