티스토리 뷰

∙Java & Spring

Junit Mock 사용하기

coor 2022. 5. 6. 19:37

들어가며

채용 공고를 보면 JUnit (Test) 자격 요건에 들어간다. 테스트는 크게 TDD와 단위 테스트가 있으며 나는 이번 쇼핑몰 프로젝트에 단위 테스트를 넣어보았다. 단위 테스트를 하려면 mock 객체가 필요하다. mock 객체가 왜 필요한지, 단위 테스트 왜 쓰는지 알아보았다.

 

1. Unit Test

코드를 작성하면 여러가지 단위 즉, 많은 메서드를 작성하게 된다. 이 많은 메서드가 정확히 돌아가는지, 예외는 제대로 터지는지, 코드를 수정하면 정상적으로 작동하는지 확인해야 한다. 이걸 일일이 웹 사이트에서 확인하면 시간과 비용이 많이 들어간다. 이때 단위 테스트를 하게 되면 개발한 코드들에 대해 수시로 빠르게 검증을 받을 수 있으며, 기능을 수정하거나 리팩토링을 할 때에도 검증을 받으므로 안정성을 확보할 수 있다는 장점이 있다.

 

2. Mock 객체

웹 애플리케이션을 개발하면 객체들 간의 의존 관계가 성립하게 된다. Mock 객체는 의존 관계가 성립된 객체를 가짜 객체로 만들어주는 것이다. 우리가 임의로 객체를 만들고 데이터를 넣어줄 수 있다. Mockito를 활용하면 가짜 객체에 원하는 결과를 Stub하여 단위 테스트를 진행할 수 있다. 

 

3. Mockito 사용법

@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserController {

	private final UserService userService;

	@PostMapping("/signup")
	public ResponseEntity<MessageResponse> registerUser(@RequestBody SignupRequest signUpRequest) {
		return userService.registerUser(signUpRequest);
	}
}

예를 들어 다음과 같은 회원 가입 API 이 있고, 이에 대한 단위 테스트를 작성해야 한다.
현재 UserService 의존 관계가 성립되어 있다. registerUser 단위 테스트를 할려면 가짜 mock 객체로 만들어줘야 한다.

@ExtendWith(MockitoExtension.class)
class UserControllerTest {

    @InjectMocks
    private UserController userController;

    @Mock
    private UserService userService;

    private MockMvc mockMvc;

    @BeforeEach
    public void init() {
        mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
    }
}

@ExtendWith(MockitoExtension.class)는 JUniit5와 Mockito를 연동하기 위해서는 사용한다. 이를 클래스 어노테이션으로 붙여 다음과 같이 테스트 클래스를 작성할 수 있다.

@InjectMocks는 주입받을 객체를 선택한다. 현재 UserController 이다.

@Mock는 가짜 객체를 주입시킨다. 현재 UserService 이다.

MockMvc는 클라이언트에서 직접 api를 요청할 수 없어서 MockMvc를 통해 HTTP 호출을 한다. 

@Test
public void 회원가입_요청() throws Exception {
    // given
    SignupRequest signupRequest = signupRequest();
    ResponseEntity<MessageResponse> responseEntity = 
                                    ResponseEntity.status(HttpStatus.OK).body(messageResponse());

    when(userService.registerUser(any(SignupRequest.class))).thenReturn(responseEntity);
}

private SignupRequest signupRequest() {
    return SignupRequest.builder()
            .username("kimjinseong")
            .email("wlsdiqkdrk@gmail.com")
            .password("123123")
            .build();
}

private MessageResponse messageResponse() {
    return MessageResponse.builder()
            .message("회원가입 완료되었습니다.")
            .build();
}

우선 회원가입 요청을 보내기 위해서는 SignUpRequest 객체 1개와 userService의 응답받을 MessageResponse에 대한 stub이 필요하다. 이러한 단계는 given 단계에서 작업한다.

when(userService.registerUser(any( ))).thenReturn(responseEntity);

이 코드를 가짜 객체를 만들어주는 코드이다.
any( ) 임의의 매개 변수를 넣어서 thenReturn( ) 임의의 데이터를 리턴을 받는 의미이다.
현재 임의의 데이터는 MessageResponse( ) 이므로 성공적으로 "회원가입 완료되었습니다." 값을 가지는 걸 볼 수 있다.

@Test
public void 회원가입_요청() throws Exception {
    // given
    SignupRequest signupRequest = signupRequest();
    ResponseEntity<MessageResponse> responseEntity = 
                                        ResponseEntity.status(HttpStatus.OK).body(messageResponse());

    when(userService.registerUser(any(SignupRequest.class))).thenReturn(responseEntity);
    
    // when
        ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.post("/api/user/signup")
                        .contentType(APPLICATION_JSON)
                        .content(new Gson().toJson(signupRequest)));
}

일반적으로 클라이언트에서 데이터는 객체가 아닌 문자열이어야 하므로 별도의 변환이 필요하므로 Gson을 사용해 변환하였다. 요청 정보에는 MockMvcRequestBuilders가 사용되며 요청 메서드 종류, 내용, 파라미터 등을 설정할 수 있다.

@Test
public void 회원가입_요청() throws Exception {
    // given
    SignupRequest signupRequest = signupRequest();
    ResponseEntity<MessageResponse> responseEntity = 
                                    ResponseEntity.status(HttpStatus.OK).body(messageResponse());

    when(userService.registerUser(any(SignupRequest.class))).thenReturn(responseEntity);
    
    // when
        ResultActions resultActions = mockMvc.perform(
                MockMvcRequestBuilders.post("/api/user/signup")
                        .contentType(APPLICATION_JSON)
                        .content(new Gson().toJson(signupRequest)));
    // then
    resultActions.andExpect(status().isOk())
                .andExpect(jsonPath("message",messageResponse().getMessage()).exists()).andReturn();

}

마지막으로 호출된 결과를 검증하는 then 단계에서는 정상적으로 200으로 호출이 되었는지 message 값은 정확히 들어왔는지 확인한다.

 

 


지금까지 mockito 기본적인 사용법에 대해 알아보았다.
이제 쇼핑몰에서 주문을 검증하는 테스트를 작성해보겠다.

주문 요구사항은
1. 정상 주문
2. 주문 수량 예외

 

1. 정상 주문

@InjectMocks
OrderServiceImpl orderService;

@Mock
OrderRepository orderRepository;

@Mock
BasketRepository basketRepository;

@Mock
UserRepository userRepository;
    
@Test
public void 정상_상품_주문_생성() throws Exception {
    // given
    when(userRepository.getById(any())).thenReturn(user());
    when(basketRepository.findAllByUserId(any())).thenReturn(basketList());

    // when
    ResponseEntity<MessageResponse> responseEntity = 
                         orderService.saveOrderDeliveryItem(user().getId(), deliveryRequestDto());

    // then
    assertEquals(responseEntity, null);
}

private User user() {
    List<Order> orders = new ArrayList<>();
    return User.builder()
            .id(1L)
            .username("kim1")
            .email("W@naver.com")
            .password("123123")
            .orders(orders)
            .build();
}

private List<Basket> basketList() {
    List<Basket> basketList = Arrays.asList(
        Basket.builder().item(item()).itemCount(2).itemTotal(30000).size("S").user(user()).build(),
        Basket.builder().item(item()).itemCount(2).itemTotal(530000).size("M").user(user()).build(),
        Basket.builder().item(item()).itemTotal(2).itemTotal(430000).size("L").user(user()).build()
    );
    return basketList;
}
 private Item item() {
    return Item.builder()
            .id(1L)
            .price(20000)
            .discountPrice(18000)
            .title("시어서커 크롭 자켓 (다크네이비)")
            .quantityS(3)
            .quantityM(3)
            .quantityL(3)
            .build();
}

현재 코드는 정상적으로 작동하는 코드로 에러 메시지가 없으면 null 값을 반환한다.

.quantityS(3)
.quantityM(3)
.quantityL(3)

상품 주문에 핵심은 수량이다. Item( ) 코드를 보면 S, M, L 수량이 각각 3개씩 들어가는 걸 볼 수 있다.

Basket.builder().item(item()).itemCount(2).itemTotal(30000).size("S").user(user()).build(),
Basket.builder().item(item()).itemCount(2).itemTotal(530000).size("M").user(user()).build(),
Basket.builder().item(item()).itemTotal(2).itemTotal(430000).size("L").user(user()).build()

Basket -> 장바구니를 의미한다. 장바구니 수량을 보면 itemCount(2) 2개가 선택되었다. 
현재 상품 수량은 3개이고 주문한 수량은 2개이므로 정상적으로 주문이 완료된다.

 

2.. 주문 수량 예외

@Test
public void 상품_주문_품절_예외() throws Exception {
    // given
    List<Basket> basketList = Arrays.asList(
        Basket.builder().item(item()).itemCount(4).itemTotal(30000).size("S").user(user()).build(),
        Basket.builder().item(item()).itemCount(2).itemTotal(530000).size("M").user(user()).build(),
        Basket.builder().item(item()).itemTotal(2).itemTotal(430000).size("L").user(user()).build()
    );
    when(userRepository.getById(any())).thenReturn(user());
    when(basketRepository.findAllByUserId(any())).thenReturn(basketList);

    // when
    assertThrows(NotEnoughStockException.class, () -> 
                         orderService.saveOrderDeliveryItem(user.getId(), deliveryRequestDto()));
}
Basket.builder().item(item()).itemCount(4).itemTotal(30000).size("S").user(user()).build(),

장바구니 상품이 itemCount(4) 4개이다. 상품의 수량은 3개이지만 장바구니 상품은 4개이므로 예외가 발생해야 한다.
현재 NotEnoughStockException 예외가 발생하는 걸 예상하고 있다. 

public void removeStock(int quantity, String size)  {
    if (size.equals("S")) {
        int restStock = this.quantityS - quantity;
        if (restStock < 0) {
            throw new NotEnoughStockException("need more stock");
        }
        this.quantityS = restStock;
    }
    if (size.equals("M")) {
        int restStock = this.quantityM - quantity;
        if (restStock < 0) {
            throw new NotEnoughStockException("need more stock");
        }
        this.quantityM = restStock;
    }
    if (size.equals("L")) {
        int restStock = this.quantityL - quantity;
        if (restStock < 0) {
            throw new NotEnoughStockException("need more stock");
        }
        this.quantityL = restStock;
    }
}

이 코드는 Item 수량과 Basket 수량을 체크하는 코드이다. 

if (restStock < 0) {
    throw new NotEnoughStockException("need more stock");
}

아이템의 수량과 장바구니 수량을 뺐을 때 0 보다 작으면 수량이 충분하지 않다는 NotEnoughStockException 예외가 발생한다. 이 예외를 통해 테스트 코드에서 예측할 수 있다.

'∙Java & Spring' 카테고리의 다른 글

동시성 이슈 해결하기  (0) 2022.09.24
ThreadPoolTaskExecutor 이용하여 성능 개선하기  (0) 2022.05.29
인프런 스프링 핵심 원리 정리  (0) 2022.04.20
JWT 구조  (0) 2022.04.16
자바의 정석 정리(2)  (0) 2022.03.09