spring-boot-3일차

박상원·2024년 5월 13일

spring

목록 보기
13/15

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"));
    }

    // If Spring WebFlux is on the classpath, you can drive MVC tests with a WebTestClient
    @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)

  • Student 시스템 통합 테스트
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  //RemoteService 를 대체하는 예제
    private RemoteService remoteService;

    @Test
    void exampleTest() {
        given(this.remoteService.getValue()).willReturn("spring");
        String reverse = this.reverser.getReverseValue(); // Calls injected RemoteService
        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")));
    }
}

Auto-configured JSON Test

  • 자동설정 환경에서 객체의 JSON 직렬화, 역직렬화를 테스트하기 위해 @JsonTest을 사용한다.
  • AssertJ 기반의 테스트 지원을 제공하기 때문에 객체-JSON 매핑의 결과를 검증할 수 있다.
  • JacksonTester, GsonTester, JsonbTester, BasicJsonTester 클래스를 각각의 JSON 라이브러리 헬퍼로 사용할 수 있다.

Auto-configured JSON Tests

  • JacksonTester 사용 예
@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");
    }

}

Auto-configured Spring MVC Tests

  • Spring MVC의 Controller를 테스트 하기 위해서는 @WebMvcTest를 사용한다.
  • @WebMvcTest를 사용하면 @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, WebMvcRegistrations, HandlerMethodArgumentResolver 등만 스캔한다.
  • 테스트에서 다른 컴포넌트를 스캔하고 싶다면 테스트코드에 @Import로 직접 설정해 주어야 한다.

Auto-configured Spring MVC Tests

  • @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

  • Student 시스템 @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")));
    }
}

Auto-configured Data JPA Tests

  • JPA를 테슨트 하기 위해서는 @DataJpaTest를 사용한다.
  • @DataJpaTest를 사용하면 @Entity, Repository만 스캔한다.
  • 기본적으로 테스트 이후에 수정된 정보는 모두 롤백한다.
  • H2와 같은 인메모리 데이터베이스가 클래스 패스에 존재하면 사용하지만 실제 데이터베이스에서 테스트하려면 @AutoConfigureTestDatabase(replace=Replace.NONE)을 설정해야 한다.

Auto-configured Data JPA Test

  • @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

  • Student 시스템 @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

자동설정(auto-configure) 모듈

  • 자동설정 모듈에는 라이브러리를 바로 사용할 수 있는 자동설정과 설정키의 정의,
  • 콜백 인터페이스를 포함한다.
  • 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 등)
  • 가능하면 고유명사를 네임스페이스로 사용할 것

0개의 댓글