Programming/Spring

[스프링부트 (10)] SpringBoot Test(3) - 단위 테스트(@WebMvcTest, @DataJpaTest, @RestClientTest 등)

byeong07 2022. 5. 29. 22:48

[스프링부트 (10)] SpringBoot Test(3) - 단위 테스트(@WebMvcTest, @DataJpaTest, @RestClientTest 등)

이번 포스팅은 [ 스프링 부트  단위 테스트 하기 (@WebMvcTest, @DataJpaTest, @RestClientTest, @JsonTest 등)입니다. : ) 

 

 

 

 

0. 들어가기 앞서

이번 포스팅의 대부의 내용도 공식 레퍼런스 문서에 더 자세하게 나와 있다.

다음 공식 문서를 꼭 참고 하면 좋을 것 같다. 
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing

 

SpringBoot는 테스트 목적에 따라 다양한 어노테이션을 제공한다. 

그중 이번엔 단위 테스트에 관련된 내용을 확인하고, 샘플소스로 테스팅 해보려 한다.

 

이전 포스팅을 안보신 분들이라면 기본 준비는 간단하다.

 

스프링 이니셜라이져로 프로젝트를 생성한 경우 이미 "spring-boot-starter-test"가 디펜던시 추가되어 있을 것이다.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
  <exclusions>
    <exclusion>
      <groupId>org.junit.vintage</groupId>
      <artifactId>junit-vintage-engine</artifactId>
  	</exclusion>
  </exclusions>
</dependency>

해당 디펜던시 하나면 테스트 준비는 완료 된 상태이다.

(나와 같은 경우 스프링 부트 2.2.2, Junit5로 테스트 하였으니 참고 하자.)

 

1. 단위 테스트

▶ @WebMvcTest

 - MVC를 위한 테스트, 컨트롤러가 예상대로 동작하는지 테스트하는데 사용된다.
(To test whether Spring MVC controllers are working as expected, use the @WebMvcTest annotation.)

 - @WebMvcTest 어노테이션을 사용시 다음 내용만 스캔 하도록 제한한다. (보다 가벼운 테스팅이 가능하다.)
@Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, HandlerMethodArgumentResolver

※ 자동 구성 설정정보 리스트는 다음 공식 문서에서 확인하며 더 최신화되고, 정확할 것이다.

https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-test-auto-configuration.html#test-auto-configuration

 

 - MockBean, MockMVC를 자동 구성하여 테스트 가능하도록 한다.
 - Spring Security의 테스트도 지원 한다.

https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-use-test-with-spring-security

 

 - @WebMvcTest를 사용하기 위해 테스트할 특정 컨트롤러 클래스를 명시 하도록 한다.

 

장점

 - WebApplication 관련된 Bean들만 등록하기 때문에 통합 테스트보다 빠르다.

 - 통합 테스트를 진행하기 어려운 테스트를 진행 가능하다.

ex) 결제 모듈 API를 콜하며 안되는 상황에서 Mock을 통해 가짜 객체를 만들어 테스트 가능.

 

 단점

 - 요청부터 응답까지 모든 테스트를 Mock 기반으로 테스트하기 때문에 실제 환경에서는 제대로 동작하지 않을 수 있다.

 

ex) @WebMvcTest 예제

package com.god.bo.test.controller;

import com.god.bo.test.service.TestService;
import com.god.bo.test.vo.TestVo;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;

import static org.hamcrest.core.Is.is;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

//@RunWith(SpringRunner.class) // ※ Junit4 사용시
@WebMvcTest(TestRestController.class)
@Slf4j
class TestRestControllerTest {

    @Autowired
    MockMvc mvc;

    @MockBean
    private TestService testService;

    @Test
    void getListTest() throws Exception {
        //given
        TestVo testVo = TestVo.builder()
                .id("goddaehee")
                .name("갓대희")
                .build();
        //given
        given(testService.selectOneMember("goddaehee"))
                .willReturn(testVo);

        //when
        final ResultActions actions = mvc.perform(get("/testValue2")
                .contentType(MediaType.APPLICATION_JSON))
                .andDo(print());

        //then
        actions
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.name", is("갓대희")))
                .andDo(print());
    }
}
java.lang.AssertionError: JSON path "$.name"
Expected: is "GOD"
     but: was "갓대희"
Expected :GOD
Actual   :갓대희

 

 

 - MockBean을 통해 가상의 조회 결과를 return 하였고, name은 "갓대희"로 선언하였다.
 - Database 관련 Bean 설정은 다 가상으로 설정하였고, Controller의 기능만 테스트 하였기 떄문에 수행 속도도 훨씬 빨라졌다.
 - 다만 테스트 결과 예측을 "GOD"으로 하였기 떄문에 오류가 발생한다.
 - given에서 해당 객체를 생성 처리한다.
 - when에서 목객체를 기반으로 미리 정의된 객체를 반환 처리한다.
 - then에서 해당 객체의 응답값을 검사 처리한다.

 - .andExpect(jsonPath("$.name", is("GOD"))) 이부분의 "GOD"을 "갓대희"로 변경하면 테스트 통과 한다.

▶ @WebFluxTest

 - 스프링 웹플럭스에 대한 이해도가 너무 낮아서 이부분은 그냥 공식 문서에 있는 내용 그대로만 발췌해서 남겨놓고 추후 자세히 포스팅 해보려 한다.
 - 비동기-논블록킹 리액티브 개발에 사용되며 서비스간 호출이 많은 마이크로 아키텍처에 적합 하다.

ex) @WebFluxTest예제 (공식 문서의 테스트 예제)

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.reactive.server.WebTestClient;

@WebFluxTest(UserVehicleController.class)
class MyControllerTests {

    @Autowired
    private WebTestClient webClient;

    @MockBean
    private UserVehicleService userVehicleService;

    @Test
    void testExample() throws Exception {
        given(this.userVehicleService.getVehicleDetails("sboot"))
                .willReturn(new VehicleDetails("Honda", "Civic"));
        this.webClient.get().uri("/sboot/vehicle").accept(MediaType.TEXT_PLAIN)
                .exchange()
                .expectStatus().isOk()
                .expectBody(String.class).isEqualTo("Honda Civic");
    }

}

▶ @DataJpaTest

 - JPA 관련된 설정만 로드합니다.
 - 설정이 정상적인지, JPA를 사용하서 데이터를 올바르게 조회, 생성, 수정, 삭제 하는지 등의 테스트가 가능하다.
 - @Entity 클래스를 스캔하여 스프링 데이터 JPA 저장소를 구성한다. ( 다른 컴포넌트를 스캔하지 않음 )
 - @Transactional 어노테이션을 포함하고 있기 때문에 따로 선언하지 않아도 됨
   @Transactional 기능이 필요하지 않으면 @Transactional(propagation = Propagation.NOT_SUPPORTED) 설정
 - 기본적으로 in-memory embedded database에 대한 테스트를 진행한다.
 - real database를 사용하고자 하는 경우@AutoConfigureTestDatabase 사용하면 된다.
   @AutoConfigureTestDatabase Default 설정 값은 Any이다. (기본적으로 내장된 데이터소스를 사용한다).
   @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)을 지정하면 실제 디비도 사용 가능하며,
   @ActiveProfiles("test") 등의 프로파일이 설정도 가능하다.

ex) @DataJpaTest예제

package com.god.bo.jpaTest.repository;

import com.god.bo.jpaTest.vo.MemberVo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

import static org.assertj.core.api.BDDAssertions.then;

@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class MemberRepositoryTest {

    @Autowired
    private MemberRepository memberRepository;

    @Test
    void findById() {
        List<MemberVo> member = memberRepository.findById("goddaehee");

        then(!member.isEmpty());

        for(MemberVo vo : member){
            then("goddaehee").isEqualTo(vo.getId());
            then("갓대희").isEqualTo(vo.getName());
        }
    }
}

 

@JdbcTest는 JPA를 사용하지 않고 기본 데이터베이스의 테스트 할 때 사용한다.

▶ @RestClientTest

참고) 공식 doc 
You can use the @RestClientTest annotation to test REST clients.
By default, it auto-configures Jackson, GSON, and Jsonb support, configures a RestTemplateBuilder, and adds support for MockRestServiceServer.
Regular @Component beans are not loaded into the ApplicationContext.

@RestClientTest 을 사용하여 REST 클라이언트 테스트가 가능하다.
REST 통신의 JSON 형식이 예상대로 응답을 반환하는지 등을 테스트 합니다.
예를 들면, Apache HttpClient나 Spring의 RestTemplate을 사용하여 외부 서버에 웹 요청을 보내는 경우에 이에 응답할 Mock서버를 만드는 것이라고 생각하면 된다.

ex)
1. 테스트를 위해 임시로 기존 MemberService에 다음 메서드를 생성 하였다.

@Service
@Slf4j
public class MemberService {
    private RestTemplate restTemplate;

    public MemberService(RestTemplateBuilder restTemplateBuilder) {
        this.restTemplate = restTemplateBuilder.build();
    }

    public MemberVo getMember(Long mbrNo) {
        MemberVo response = restTemplate.getForObject("/memberTest/" + mbrNo, MemberVo.class);
        log.info("getMember2 : " + response);
        return response;
    }
}

 

2. classPath 하위에 다음 내용의 test.json파일을 생성 하였다.

{"id":"goddaehee2","name":"갓대희"}

 

 

 

3. 테스트 소스

ex) @RestClientTest예제

package com.god.bo.jpaTest.service;

import com.god.bo.jpaTest.repository.MemberRepository;
import com.god.bo.jpaTest.vo.MemberVo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.client.RestClientTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.core.io.ClassPathResource;
import org.springframework.http.MediaType;
import org.springframework.test.web.client.MockRestServiceServer;

import static org.assertj.core.api.BDDAssertions.then;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;

@RestClientTest(MemberService.class)
class MemberServiceTest {
    @MockBean
    private MemberRepository memberRepository;

    @Autowired
    private MockRestServiceServer server;

    @Autowired
    private MemberService memberService;

    @Test
    void getMember() {
        server.expect(requestTo("/memberTest/1"))
                .andRespond(withSuccess(new ClassPathResource("/test.json", getClass()), MediaType.APPLICATION_JSON));

        //MemberVo member = memberService.getMember(1L);
        MemberVo member = memberService.getMember(1L);

        // ※ Junit4 사용시
        // assertThat("goddaehee2").isEqualTo(member.getId());
        // assertThat("갓대희"").isEqualTo(member..getName());

        // Junit5 BDD 사용시
        then("goddaehee2").isEqualTo(member.getId());
        then("갓대희").isEqualTo(member.getName());
    }
}

▶ @JsonTest

 - @JsonTest는 JSON serialization과 deserialization 테스트를 편하게 할 수 있다.
 - JSON의 직렬화, 역직렬화를 수행하는 라이브러인 Gson과 Jackson의 테스트를 제공한다.
(JacksonTester, GsonTest, BasicJsonTester)

ex) @JsonTest예제(공식 문서의 테스트 예제)

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.*;
import org.springframework.boot.test.autoconfigure.json.*;
import org.springframework.boot.test.context.*;
import org.springframework.boot.test.json.*;

import static org.assertj.core.api.Assertions.*;

@JsonTest
class MyJsonTests {

    @Autowired
    private JacksonTester<VehicleDetails> json;

    @Test
    void testSerialize() throws Exception {
        VehicleDetails details = new VehicleDetails("Honda", "Civic");
        // Assert against a `.json` file in the same package as the test
        assertThat(this.json.write(details)).isEqualToJson("expected.json");
        // Or use JSON path based assertions
        assertThat(this.json.write(details)).hasJsonPathStringValue("@.make");
        assertThat(this.json.write(details)).extractingJsonPathStringValue("@.make")
                .isEqualTo("Honda");
    }

    @Test
    void testDeserialize() 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");
    }
}

 

언젠간 지금 하고 있는 운영 업무에도 TDD를 적용하여 협업하면 좋을 것 같아 정리해 보았다.

개발 하면서 개발된 소스에 대해 단위 테스트를 하는 습관을 들이면 좋을 것이다.