[JUnit5] 통합 테스트(@SpringBootTest), @MockBean

u-nij·2022년 11월 2일
2

TDD

목록 보기
2/3
post-thumbnail
post-custom-banner

통합 테스트

실제 운영 환경에서 사용될 클래스들을 통합해 테스트한다. 단위 테스트처럼 기능 검증을 위한 것이 아닌, 전체적으로 플로우가 제대로 동작하는지 검증하기 위해 사용한다. 애플리케이션 설정과 Bean들을 모두 로드해 운영환경과 가장 유사한 테스트가 가능하다는 장점이 있지만, 시간이 오래 걸리고 무겁다는 단점이 있다. 또, 단위가 크기 때문에 디버깅이 어려운 편이다.

build.gradle

	testImplementation 'org.springframework.boot:spring-boot-starter-test'

@SpringBootTest

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
class SampleControllerTest {@Autowired
   MockMvc mockMvc;@Test
   public void hello() throws Exception {
       mockMvc.perform(get("/hello"))
          .andExpect(status().isOk())
          .andExpect(content().string("hello namjune"))
          .andDo(print());
  }

@SpringBootApplication을 찾아서 테스트를 위한 빈들을 다 생성한다. 그리고, @MockBean으로 정의된 빈을 찾아서 교체한다.

webEnvironment

웹 테스트 환경 구성이 가능하다. default 값은 MOCK이다.

MOCK

WebApplicationContext를 로드하며, 내장된 서블릿 컨테이너가 아닌 Mock 서블릿을 제공한다. @AutoConfigureMockMvc 어노테이션을 함께 사용하면 별다른 설정 없이 간편하게 MockMvc를 사용한 테스트를 진행할 수 있다. MockMvc는 실제 객체와 비슷하지만 테스트에 필요한 기능만 가지는 가짜 객체를 만들어 스프링 MVC 동작을 재현할 수 있게 해주는 클래스이다. 브라우저에서 요청과 응답을 의미하는 객체로써, Controller 테스트 사용을 용이하게 해준다.

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class XXXTest {
    @Autowired
    public MockMvc mockMvc;
}

RANDOM_PORT / DEFINED_PORT

EmbeddedWebApplicationContext를 로드하며 실제 서블릿 환경을 구성한다. MockMvc 대신, RestTemplate를 사용할 수 있다. 실제 가용한 포트로 내장 톰캣을 띄우고 응답을 받아, 실제 서버가 동작하는 것처럼 테스트를 수행할 수 있다.

TestRestTemplate
RestTemplate의 테스트를 위한 버전이다. NONE 설정을 제외한 webEnvironment 설정시 그에 맞춰서 자동으로 설정되어 빈이 생성된다. HTTP 요청 후, JSON, xml, String과 같은 응답을 받을 수 있는 템플릿이다.

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class XXXTest {
    @Autowired 
    private TestRestTemplate restTemplate;
}

NONE

서블릿 환경을 제공하지 않는다.

MockMvc과 TestRestTemplate의 차이

Servlet Container
MockMvc는 서블릿 컨테이너를 생성하지 않는 반면, TestRestTemplate은 서블릿 컨테이너를 사용한다. 그래서 마치 실제 서버가 동작하는 것처럼 테스트를 수행할 수 있다.(몇몇 Bean은 Mock 객체로 대체될 수는 있다.)

테스트 관점
MockMvc는 서버 입장에서 구현한 API를 통해 비즈니스 로직이 문제 없이 수행되는지 테스트할 수 있다면, TestRestTemplate은 클라이언트의 입장에서 RestTemplate을 사용하듯이 테스트를 수행할 수 있다.

properties

프로퍼티를 {key=value} 형식으로 직접 추가할 수 있다.

@SpringBootTest(
	properties = {
		"propertyTest.value=propertyTest"
	}
)
class XXXTest {

    @Value("${propertyTest.value}")
	private String propertyValue; // 값: "propertyTest"
    
}

기본적으로 클래스 경로상의 application.properties 또는 application.yml를 통해 애플리케이션 설정을 하지만, 테스트를 위한 다른 설정이 필요할 경우 다른 프로퍼티를 로드할 수 있다.

@SpringBootTest(
	properties = {
		"spring.config.location=classpath:application-test.yml"
	}
)
class XXXTest {
	// ...
}

@MockBean

Controller 테스트 코드에서 Service 단으로 흘러 들어갈 경우, 테스트 단위가 너무 커지게 되고 구동 시간이 오래 걸린다. 만약 Controller만 테스트 하고 싶을 경우, Service 객체를 MockBean으로 만들어 사용할 수 있다. @MockBean 어노테이션을 사용하게 되면, Spring Application Context에 들어있는 Bean을 Mock으로 만든 객체(가짜 객체)로 교체한다. 모든 @Test마다 자동으로 리셋된다. 테스트할 때 Spring Boot Container가 필요하고 Bean이 Container에 존재한다면 @MockBean을 사용하고, 아닌 경우에는 @Mock을 쓰면 된다.

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class XXXTest {
    @Autowired
    public MockMvc mockMvc;
    
    @MockBean
    public TestService testService;
}

슬라이스 테스트

계층(layer) 별로 테스트하고 싶을 때 사용하게 되는 방법이다. 글의 시작 부분에 적은 것처럼@SpringBootTest의 경우 모든 Bean을 로드하는 통합 테스트이기 때문에, 테스트 구동 시간이 오래 걸리는 단점이 있다. 특정 계층만을 테스트하고 싶을 때 사용하면 유용하다. @JsonTest, @WebFluxTest, @DataJpaTest 등 다양한 어노테이션이 있지만, @WebMvcTest만 간단하게 적고 넘어가겠다.
@WebMvcTest
Application Context을 전체 로드해 사용하지 않고, Web 계층을 테스트하고 싶을 때 사용한다. Service, Repository 등 다른 dependency가 필요한 경우에 @MockBean으로 주입받아 테스트를 진행한다.

적용

공통으로 사용할 클래스

SimpleController

@RestController
@RequestMapping("/api/test")
public class SimpleController {

    @GetMapping("/")
    public ResponseEntity<String> getTest() {
        return ResponseEntity.ok("hello");
    }

    @PostMapping("/")
    public ResponseEntity<Map<String, String>> postTest(@RequestBody String request) {
        Map<String, String> body = new HashMap<>();
        body.put("response", request);
        return ResponseEntity.ok().body(body);
    }

    @GetMapping("/dto")
    public ResponseEntity<List<TestDto>> returnDtoTest() {
        TestDto testDto1 = TestDto.builder()
                .name("테스터1")
                .email("test1@test.co.kr")
                .build();
        TestDto testDto2 = TestDto.builder()
                .name("테스터2")
                .email("test2@test.co.kr")
                .build();
        List<TestDto> body = new ArrayList<>();
        body.add(testDto1);
        body.add(testDto2);
        return ResponseEntity.ok().body(body);
    }
}

MockMvcTestRestTemplate

TestDto

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class TestDto {
    private String name;
    private String email;

    @Builder
    public TestDto(String name, String email) {
        this.name = name;
        this.email = email;
    }
}

webEnvironment별 테스트 클래스

MOCK

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
class SimpleMockControllerTest {

    @Autowired
    public MockMvc mockMvc;

    @Test
    public void GET_테스트() throws Exception {
        mockMvc.perform(get("/api/test/"))
                .andExpect(status().isOk());
    }

    @Test
    public void POST_테스트() throws Exception {
        String request = "test";

        mockMvc.perform(post("/api/test/")
                        .content(request)
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.response").value(request));
    }

    @Test
    public void DTO_리턴_테스트() throws Exception {
        mockMvc.perform(get("/api/test/dto")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.[0].name").value("테스터1"))        
                .andExpect(jsonPath("$.[1].name").value("테스터2"));
    }
}

RANDOM_PORT

@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SimpleRandomPortControllerTest {

    @Autowired
    public TestRestTemplate testRestTemplate;

	// 응답 Body를 JsonNode로 변환해주는 메소드
    public JsonNode readInfo(String content) {
        try {
            return new ObjectMapper().readTree(content);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("JsonProcessingException");
        }
    }

    @Test
    public void GET_테스트() throws Exception {
        ResponseEntity<String> response = testRestTemplate.getForEntity("/api/test/", String.class);
        assertThat(response.getStatusCode().value()).isEqualTo(200);
    }

    @Test
    public void POST_테스트() throws Exception {
        String request = "test";

        ResponseEntity<String> response = testRestTemplate.postForEntity("/api/test/", request, String.class);
        JsonNode responseInfo = readInfo(response.getBody());

        assertThat(response.getStatusCodeValue()).isEqualTo(200);
        assertThat(responseInfo.get("response").asText()).isEqualTo(request);
    }

    @Test
    public void DTO_리턴_테스트() throws Exception {

        ResponseEntity<String> response = testRestTemplate.getForEntity("/api/test/dto", String.class);
        JsonNode responseInfo = readInfo(response.getBody());

        assertThat(response.getStatusCodeValue()).isEqualTo(200);
        assertThat(responseInfo.get(0).get("name").asText()).isEqualTo("테스터1");
        assertThat(responseInfo.get(1).get("name").asText()).isEqualTo("테스터2");
    }
}

참고

https://ict-nroo.tistory.com/96
https://www.inflearn.com/blogs/339

profile
삶은 달걀이다
post-custom-banner

0개의 댓글