본 포스팅은, 단위 테스트 (Unit Test) 와 관련된 내용입니다.
작성된 내용 외에도 다양한 방법이 있으며, 정답은 없습니다.
<목차>
1. 컨트롤러 단위 테스트
2. 테스트 코드
3. GitHub
SpringBoot 환경에서 전체적인 비즈니스 로직이 정상적으로 동작하는지 테스트 하기 위해서는 통합 테스트를 해야 하고, 각 모듈을 테스트 하고 싶다면, 단위 테스트를 진행해야 한다.
일반적인 Spring MVC 를 사용하는 구조라면, 일부 레이어에서만 단위 테스트를 수행하는게 무의미하거나 어려운 경우가 많다.
하지만! Mock 객체를 활용하여, 하위 레이어와 무관하게 동작하도록, 레이어 별 단위 테스트 코드를 작성해보도록 하겠다.
앞서 JPA Example 에서 작성한 소스를 기반으로 작성된 포스팅이며,
Controller - Service - DAO - Repository 구조에서, DAO 를 제외한 구조로 재구성 하였다.
2024.08.04 - [Programming/Spring & Spring boot] - [Spring Boot] JPA - 4. CRUD Example
[Spring Boot] JPA - 4. CRUD Example
1. Environment2. Sources3. Test Springboot - Basic JPA CRUD API Example JPA를 바로 사용하기 보단, 동작 원리에 대해 알고 사용하는 것이 도움이 된다.JPA 관련 포스팅https://jh4dev.tistory.com/24 [Spring Boot] JPA - 1. ORM
jh4dev.tistory.com
[컨트롤러 테스트]
- Controller 는 클라이언트로부터 요청을 받아 요청에 걸맞은 서비스 컴포넌트로 요청을 전달하고 그 결과값을 가공하여 클라이언트에게 응답하는 역할을 수행한다.
- 일반적인 구조에서, 요청 값을 통해 서비스 메서드를 호출하고 응답을 리턴하는 구조이다.
- Spring 환경의 Controller 테스트에서 고려할 부분으로는 아래 내용이 있다.
- 단위 테스트를 위해, 필요한 대상 클래스만 로드
- @WebMvcTest 어노테이션을 활용한 Slice Test 진행
- @WebMvcTest 어노테이션을 활용한 Slice Test 진행
- Service 객체 DI (의존성 주입)
- @MockBean 어노테이션을 활용하여 Mock 객체를 생성하여 주입
- 컨트롤러를 호출하는 액션
- MockMvcRequestBuilders.perform() 메서드 사용
- 단위 테스트를 위해, 필요한 대상 클래스만 로드
[테스트 코드]
테스트를 진행할 Controller 는 다음과 같다.
import com.springboot.test.data.dto.ChangeProductNameDto;
import com.springboot.test.data.dto.ProductDto;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/product")
public class ProductController {
private final ProductService productService;
@Autowired
public ProductController(ProductService productService) {
this.productService = productService;
}
@GetMapping
public ResponseEntity<ProductResponseDto> getProduct(Long number) {
log.info("ProductController.getProduct() 호출!!");
ProductResponseDto productResponseDto = productService.getProduct(number);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
@PostMapping
public ResponseEntity<ProductResponseDto> createProduct(@RequestBody ProductDto productDto) {
log.info("ProductController.createProduct() 호출!!");
ProductResponseDto productResponseDto = productService.saveProduct(productDto);
return ResponseEntity.status(HttpStatus.OK).body(productResponseDto);
}
}
테스트할 ProductController 를 살펴보자.
- DI (의존성 주입)
- ProductService 를 주입받아 사용하고 있다.
- getProduct()
- Product Entity 의 Id 값인 number 필드를 통해, Product 인스턴스를 조회하여 Response 로 리턴하는 컨트롤러 메서드
- createProduct()
- JSON 형태의 Request를 ProductDto 로 받아 DB에 저장한 후, 저장된 Product Entity 를 Response 로 리턴하는 컨트롤러 메서드
이러한 ProductController 에 대하여, Mock 객체를 사용하는 단위 테스트 코드는 아래와 같이 작성할 수 있다.
package com.springboot.test.controller;
import com.google.gson.Gson;
import com.springboot.test.data.dto.ProductDto;
import com.springboot.test.data.dto.ProductResponseDto;
import com.springboot.test.service.ProductService;
import org.junit.jupiter.api.DisplayName;
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 static org.mockito.Mockito.verify;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.mockito.BDDMockito.given;
@WebMvcTest(ProductController.class)
public class ProductControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
ProductService productService;
@Test
@DisplayName("MockMvc 를 통한 Product 데이터 조회 테스트")
void getProductTest() throws Exception {
given(productService.getProduct(123L)).willReturn(new ProductResponseDto(123L, "pen", 5000, 200));
String productId = "123";
mockMvc.perform(get("/product?number=" + productId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.number").exists())
.andExpect(jsonPath("$.price").exists())
.andExpect(jsonPath("$.stock").exists())
.andDo(print());
verify(productService).getProduct(123L);
}
@Test
@DisplayName("Product 데이터 생성 테스트")
void createProductTest() throws Exception {
//Mock 객체에서 특정 메소드가 실행되는 경우 실제 Return을 줄 수 없기 때문에 아래와 같이 가정 사항을 만들어줌
given(productService.saveProduct(new ProductDto("pen", 5000, 2000)))
.willReturn(new ProductResponseDto(12315L, "pen", 5000, 2000));
ProductDto productDto = ProductDto.builder()
.name("pen")
.price(5000)
.stock(2000)
.build();
Gson gson = new Gson();
String content = gson.toJson(productDto);
mockMvc.perform(
post("/product")
.content(content)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.number").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
.andExpect(jsonPath("$.stock").exists())
.andDo(print());
verify(productService).saveProduct(new ProductDto("pen", 5000, 2000));
}
}
작성된 테스트 코드에 대해 하나씩 살펴보자.
- @WebMvcTest 어노테이션
- Web에서 사용되는 Request와 Response에 대한 테스트를 수행할 수 있다.
- 기본적으로 @Controller / @RestController / @ControllerAdvice 등 컨트롤러 관련 빈 객체를 모두 로드하며, @WebMvcTest(ProductContoller.class) 와 같이 테스트할 대상 클래스를 지정하면, 지정한 클래스만 로드하여 테스트를 수행한다.
- @MockMvc 어노테이션
- MockMvc는 컨트롤러의 API를 테스트하기 위해 사용된 객체이다.
- 서블릿 컨테이너 구동 없이, 가상의 MVC 환경에서 모의 HTTP 서블릿을 요청하는 유틸리티 클래스
- 41번 라인 / 68번 라인 을 보면, perfrom() 메서드를 활용하여 서버로 요청을 보내는 것처럼 통신 테스트 코드를 작성할 수 있다.
- perform() 메서드는 MockMvcRequestBuilders 에서 제공하는 HTTP 메서드로, GET/POST/PUT/DELETE 에 매핑되는 메서드를 제공한다.
- ResultActions 객체를 리턴하며, 42번 라인 / 72번 라인과 같이 andExpect() 메서드로 결과 검증을 수행할 수 있다.
- 요청과 응답의 전체 내용을 확인하려면, andDo() 메서드를 사용한다.
- @MockBean 어노테이션
- @MockBean 은 실제 Bean 객체가 아닌 Mock 객체를 생성하여 주입하는 역할을 수행한다.
어디까지나 가짜 객체이기 때문에, 실제 행위를 수행하지 않으며, Mockito의 when() 또는 given() 메서드를 통하여 해당 Mock객체의 동작을 정의해야 한다 - 37번 라인 / 58번 라인 을 보면, given() 메서드를 통해 이 객체에서 어떤 메서드가 호출되고 파라미터는 무엇인지 "가정"한 후, willReturn() 메서드를 통해 메서드가 리턴하는 결과를 "직접 지정"하는 구조로 작성한다.
- @MockBean 은 실제 Bean 객체가 아닌 Mock 객체를 생성하여 주입하는 역할을 수행한다.
- verify() 메서드
- 지정된 메서드가 실행되었는지 검증하는 역할을 수행한다.
- 일반적으로 given() 에 정의된 동작과 대응한다.
<테스트 결과>
- getProductTest()
- createProductTest()
[GitHub]
[참고 도서]

<스프링 부트 핵심 가이드>
저자 : 장정우
출판사 : 위키북스
'Spring & Spring boot' 카테고리의 다른 글
[Springboot] 테스트 코드 - 8 - Repository Unit Test 리포지토리 단위 테스트 (0) | 2024.08.08 |
---|---|
[Springboot] 테스트 코드 - 7 - Service Unit Test 서비스 단위 테스트 (0) | 2024.08.07 |
[Springboot] 테스트 코드 - 5 - Mockito (0) | 2024.08.06 |
[Springboot] 테스트 코드 - 4 - Assertion 단정문 (0) | 2024.08.06 |
[Springboot] 테스트 코드 - 3 - JUnit5 (1) | 2024.08.06 |