Userservice 부분에서의 계층별로 검증하는 구조
“Spring Boot 기반 MSA에서 userservice를 대상으로
- 애플리케이션 컨텍스트가 정상 기동하는지,
- 컨트롤러 API가 올바른 응답을 주는지,
- 서비스 로직이 외부 의존성과 분리된 채 맞게 동작하는지,
- 주문 서비스와의 호출 경로 계약이 맞는지
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 메서드
- 서비스 로직 없이 바로 문자열 응답하는 부분
- 불필요한 비즈니스 로직 호출이 없는지 확인
'분산 처리 환경' 카테고리의 다른 글
| 스프링 분산 처리 환경 19: Spring Cloud Config 설정 분리 및 Docker + GitHub Actions CI/CD 트러블슈팅 (1) | 2026.03.24 |
|---|---|
| 스프링 분산 처리 환경 18: Github Action (0) | 2026.02.10 |
| 스프링 분산 처리 환경 17: Feign Client (0) | 2026.02.08 |
| 스프링 분산 처리 환경 16: Config Server (0) | 2026.01.23 |
| 스프링 분산 처리 환경 15: MSA (0) | 2026.01.15 |