분산 처리 환경

스프링 분산 처리 환경 20: 통합 테스트

ohji52 2026. 3. 30. 16:14

Userservice 부분에서의 계층별로 검증하는 구조

 

“Spring Boot 기반 MSA에서 userservice를 대상으로

  1. 애플리케이션 컨텍스트가 정상 기동하는지,
  2. 컨트롤러 API가 올바른 응답을 주는지,
  3. 서비스 로직이 외부 의존성과 분리된 채 맞게 동작하는지,
  4. 주문 서비스와의 호출 경로 계약이 맞는지

Mock

테스트할 때 진짜 객체 대신 넣는 가짜 객체 

객체가 돌려줄 값을 가정하고 만드는 것

하나의 기능만 테스트하기에 적합

 

MockMvc

서버를 띄우지 않고 HTTP 요청을 흉내 내는 도구

원래 api 테스트 과정  브라우저 → 서버 → 컨트롤러 이지만

mockmvc를 사용해 테스트 코드 → (가짜 HTTP 요청) → 컨트롤러

서버 없이 api 테스트가 가능해짐

MockBean

해당 빈을 가짜 빈으로 교체

가짜 빈을 생성해서 주입

가짜 객체가 컨트롤러에 주입되면서 서비스 로직과 분리된 상태에서의 테스트가 가능해짐

package com.distributed.userservice.service;

import com.distributed.userservice.client.OrderServiceClient;
import com.distributed.userservice.domain.User;
import com.distributed.userservice.dto.ResponseOrder;
import com.distributed.userservice.dto.UserDto;
import com.distributed.userservice.repository.UserRepository;
import com.distributed.userservice.util.JwtTokenProvider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.List;
import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @Mock
    private JwtTokenProvider tokenProvider;

    @Mock
    private PasswordEncoder passwordEncoder;

    @Mock
    private OrderServiceClient orderServiceClient;

    @InjectMocks
    private UserService userService;

    private User user;

    @BeforeEach
    void setUp() {
        user = new User();
        user.setUserId("user-123");
        user.setUsername("alice");
        user.setEmail("alice@example.com");
        user.setName("Alice");
        user.setPassword("encoded-password");
    }

    @BeforeEach
    void printTestName(TestInfo testInfo) {
        System.out.println("[TEST] " + testInfo.getDisplayName());
    }

    @Test
    void getUserByUserId_returnsUserWithOrders() {
        ResponseOrder order = new ResponseOrder();
        order.setOrderId("order-1");

        when(userRepository.findByUserId("user-123")).thenReturn(Optional.of(user));
        when(orderServiceClient.getOrders("user-123")).thenReturn(List.of(order));

        UserDto result = userService.getUserByUserId("user-123");

        assertThat(result.getUserId()).isEqualTo("user-123");
        assertThat(result.getUsername()).isEqualTo("alice");
        assertThat(result.getOrders()).hasSize(1);
        assertThat(result.getOrders().get(0).getOrderId()).isEqualTo("order-1");
    }

    @Test
    void getUserByUserId_throwsWhenUserDoesNotExist() {
        when(userRepository.findByUserId("missing-user")).thenReturn(Optional.empty());

        assertThatThrownBy(() -> userService.getUserByUserId("missing-user"))
                .isInstanceOf(UsernameNotFoundException.class)
                .hasMessage("User not found");
    }

    @Test
    void createUser_savesEncodedPassword() {
        UserDto request = new UserDto();
        request.setUsername("alice");
        request.setEmail("alice@example.com");
        request.setName("Alice");
        request.setPassword("plain-password");

        when(passwordEncoder.encode("plain-password")).thenReturn("encoded-password");
        when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));

        UserDto result = userService.createUser(request);

        ArgumentCaptor<User> savedUser = ArgumentCaptor.forClass(User.class);
        verify(userRepository).save(savedUser.capture());

        assertThat(result.getUserId()).isNotBlank();
        assertThat(savedUser.getValue().getPassword()).isEqualTo("encoded-password");
        assertThat(savedUser.getValue().getUserId()).isEqualTo(result.getUserId());
    }

    @Test
    void authenticateAndGenerateToken_returnsTokenWhenPasswordMatches() {
        when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user));
        when(passwordEncoder.matches("plain-password", "encoded-password")).thenReturn(true);
        when(tokenProvider.createToken("alice")).thenReturn("jwt-token");

        String token = userService.authenticateAndGenerateToken("alice", "plain-password");

        assertThat(token).isEqualTo("jwt-token");
    }

    @Test
    void authenticateAndGenerateToken_throwsWhenPasswordDoesNotMatch() {
        when(userRepository.findByUsername("alice")).thenReturn(Optional.of(user));
        when(passwordEncoder.matches("wrong-password", "encoded-password")).thenReturn(false);

        assertThatThrownBy(() -> userService.authenticateAndGenerateToken("alice", "wrong-password"))
                .isInstanceOf(RuntimeException.class)
                .hasMessage("Invalid password.");
    }
}

 

 

getUserByUserId_returnsUserWithOrders()

  • UserService.getUserByUserId() 동작 검증
  • DB 조회 결과를 정상적으로 처리하는지
  • 외부 주문 서비스 응답을 받아오는지 (Mock 기반)
  • 사용자 + 주문 데이터를 하나로 조합하는지
  • 최종 UserDto에 주문 목록이 포함되는지

getUserByUserId_throwsWhenUserDoesNotExist()

  • 사용자 조회 실패 상황 테스트
  • DB에서 사용자가 없을 때 처리 검증
  • UsernameNotFoundException 발생 여부 확인
  • 예외 메시지 정확성 검증

createUser_savesEncodedPassword()

  • UserService.createUser() 동작 검증
  • 회원가입 요청 DTO 처리 확인
  • 비밀번호가 암호화되는지 확인
  • DB 저장 시 암호화된 값이 들어가는지
  • userId가 생성되는지
  • 저장된 데이터와 반환 DTO 일관성 검증

 

 

 authenticateAndGenerateToken_returnsTokenWhenPasswordMatches()

  • 로그인 성공 흐름 테스트
  • 사용자 조회 정상 동작 확인
  • 비밀번호 비교 로직 검증
  • JWT 토큰 생성 여부 확인
  • 최종 토큰 반환 검증

 

 

 authenticateAndGenerateToken_throwsWhenPasswordDoesNotMatch()

  • 로그인 실패 흐름 테스트
  • 사용자 존재하지만 비밀번호 불일치 상황
  • 비밀번호 비교 실패 처리 확인
  • 예외 발생 여부 및 메시지 검증
  • 토큰이 생성되지 않는지 확인

package com.distributed.userservice.controller;

import com.distributed.userservice.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
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.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(AuthController.class)
@Import(com.distributed.userservice.config.SecurityConfig.class)
class AuthControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @BeforeEach
    void printTestName(TestInfo testInfo) {
        System.out.println("[TEST] " + testInfo.getDisplayName());
    }

    @Test
    void login_returnsTokenWhenAuthenticationSucceeds() throws Exception {
        when(userService.authenticateAndGenerateToken("alice", "plain-password")).thenReturn("jwt-token");

        mockMvc.perform(post("/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "username": "alice",
                                  "password": "plain-password"
                                }
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.success").value(true))
                .andExpect(jsonPath("$.message").value("로그인 성공"))
                .andExpect(jsonPath("$.token").value("jwt-token"));
    }

    @Test
    void login_returnsUnauthorizedWhenAuthenticationFails() throws Exception {
        when(userService.authenticateAndGenerateToken("alice", "wrong-password"))
                .thenThrow(new RuntimeException("Invalid password."));

        mockMvc.perform(post("/login")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "username": "alice",
                                  "password": "wrong-password"
                                }
                                """))
                .andExpect(status().isUnauthorized())
                .andExpect(jsonPath("$.success").value(false))
                .andExpect(jsonPath("$.message").value("Invalid password."))
                .andExpect(jsonPath("$.token").doesNotExist());
    }
}

 

 

@Import(SecurityConfig.class)

  • 보안 설정을 테스트 환경에 포함
  • 실제 애플리케이션의 Security 설정 반영
  • 로그인 API가 보안 설정 안에서 정상 동작하는지 확인

login_returnsTokenWhenAuthenticationSucceeds()

  • 로그인 성공 상황 테스트
  • AuthController의 /login POST API 검증
  • userService.authenticateAndGenerateToken() 성공 결과 가정
  • JSON 요청 본문이 정상적으로 전달되는지 확인
  • HTTP 200 응답 여부 확인
  • 응답 JSON의 success, message, token 값 검증

 

  • AuthController 안의 로그인 처리 메서드
  • 요청 body를 받아 username, password 추출하는 부분
  • userService.authenticateAndGenerateToken() 호출 부분
  • 서비스 결과를 성공 응답 JSON으로 변환하는 부분

login_returnsUnauthorizedWhenAuthenticationFails()

  • 로그인 실패 상황 테스트
  • userService.authenticateAndGenerateToken() 예외 발생 가정
  • 비밀번호 불일치 상황 구성
  • HTTP 401 응답 여부 확인
  • 응답 JSON의 success, message 값 검증
  • token 필드가 없어야 하는지 확인

 

  • AuthController 안의 로그인 처리 메서드
  • 서비스에서 예외가 발생했을 때의 처리 로직
  • 실패 응답 JSON 생성 부분
  • HTTP 상태코드를 401로 내려주는 부분

package com.distributed.userservice.controller;

import com.distributed.userservice.dto.UserDto;
import com.distributed.userservice.service.UserService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
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.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
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.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
@Import(com.distributed.userservice.config.SecurityConfig.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @BeforeEach
    void printTestName(TestInfo testInfo) {
        System.out.println("[TEST] " + testInfo.getDisplayName());
    }

    @Test
    void getUser_returnsUserDetails() throws Exception {
        UserDto response = new UserDto();
        response.setUserId("user-123");
        response.setUsername("alice");
        response.setEmail("alice@example.com");

        when(userService.getUserByUserId("user-123")).thenReturn(response);

        mockMvc.perform(get("/users/user-123"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.userId").value("user-123"))
                .andExpect(jsonPath("$.username").value("alice"))
                .andExpect(jsonPath("$.email").value("alice@example.com"));
    }

    @Test
    void createUser_returnsCreatedUser() throws Exception {
        UserDto response = new UserDto();
        response.setUserId("user-123");
        response.setUsername("alice");

        when(userService.createUser(org.mockito.ArgumentMatchers.any(UserDto.class))).thenReturn(response);

        mockMvc.perform(post("/users/signup")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {
                                  "username": "alice",
                                  "email": "alice@example.com",
                                  "name": "Alice",
                                  "password": "plain-password"
                                }
                                """))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.userId").value("user-123"))
                .andExpect(jsonPath("$.username").value("alice"));
    }

    @Test
    void healthCheck_returnsWorkingMessage() throws Exception {
        mockMvc.perform(get("/users/health_check"))
                .andExpect(status().isOk())
                .andExpect(content().string("It's Working in User Service"));

        verify(userService, org.mockito.Mockito.never()).getUserByUserId(org.mockito.ArgumentMatchers.anyString());
    }
}

 

 

getUser_returnsUserDetails()

  • 사용자 조회 API 테스트
  • GET /users/{userId} 요청 처리 검증
  • userService.getUserByUserId() 결과를 응답으로 잘 변환하는지 확인
  • HTTP 200 상태코드 검증
  • userId, username, email JSON 응답 값 검증

 

  • UserController 안의 사용자 조회 메서드
  • path variable userId를 받는 부분
  • userService.getUserByUserId() 호출 부분
  • 반환 DTO를 JSON 응답으로 내려주는 부분

 

createUser_returnsCreatedUser()

  • 회원가입 API 테스트
  • POST /users/signup 요청 처리 검증
  • 요청 JSON을 UserDto로 바인딩하는지 확인
  • userService.createUser() 결과를 응답으로 잘 변환하는지 확인
  • HTTP 201 상태코드 검증
  • 생성된 사용자 정보(userId, username) 응답 검증
  •  
  • UserController 안의 회원가입 메서드
  • 요청 body를 DTO로 받는 부분
  • userService.createUser() 호출 부분
  • 생성 결과를 201 Created로 반환하는 부분

 

healthCheck_returnsWorkingMessage()

  • 헬스체크 API 테스트
  • GET /users/health_check 요청 처리 검증
  • HTTP 200 상태코드 검증
  • 문자열 응답 "It's Working in User Service" 검증
  • userService.getUserByUserId()가 호출되지 않는지 확인

 

  • UserController 안의 health check 메서드
  • 서비스 로직 없이 바로 문자열 응답하는 부분
  • 불필요한 비즈니스 로직 호출이 없는지 확인