2025. 2. 27. 10:27ㆍBackend/Spring
개요
GitHub - mixedsider/spring-advanced: [내배캠] 심화 주차 개인 과제
[내배캠] 심화 주차 개인 과제. Contribute to mixedsider/spring-advanced development by creating an account on GitHub.
github.com
내배캠의 Lv.6 도전과제로 테스트 커버리지를 채우는 과제 내용이 있었다.
하면서 얻은 느낀점과 아쉬운점, 방법 등에 대해서 서술해보고자 한다.
테스트 코드란
테스트 코드(Test code)는 소프트웨어의 기능과 동작을 테스트하는 데 사용되는 코드이다.
잘 작성된 테스트 코드는 예상치 못한 문제를 미리 발견을 하게 해주고, 코드 수정이 필요한 상황에서 유연하고 안정적인 대응을 할 수 있다.즉, 서비스의 품질 및 코드의 안정성을 빠르게 챙길 수 있다는 점이다.
예시
UserController
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping("/{userId}")
public ResponseEntity<UserResponse> getUser(@PathVariable long userId) {
return ResponseEntity.ok(userService.getUser(userId));
}
@PutMapping("")
public ResponseEntity<Void> changePassword(@Auth AuthUser authUser, @RequestBody UserChangePasswordRequest userChangePasswordRequest) {
userService.changePassword(authUser.getId(), userChangePasswordRequest);
return new ResponseEntity<>(HttpStatus.OK);
}
}
위의 UserController에 대한 테스트 코드를 작성을 해볼까 한다.
왜냐하면 그나마 이해를 했기 때문에..
UserController는 두개의 메소드만 가지고 있는다.
getUser 는 유저 정보 단건 조회이다.
changePassword 메소드명 그대로 비밀번호를 변경하는 API 이다.
UserControllerTest
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void User_단일_조회() throws Exception {
//given
long userId = 1L;
String email = "test@test.com";
UserResponse userResponse = new UserResponse(userId, email);
given(userService.getUser(userId)).willReturn(userResponse);
// when && then
MvcResult mvcResult = mockMvc.perform(get("/users/{userId}", userId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(userId))
.andExpect(jsonPath("$.email").value(email))
.andReturn();
// then (ResponseEntity 검증)
MockHttpServletResponse response = mvcResult.getResponse();
assertEquals(HttpStatus.OK.value(), response.getStatus()); // HTTP 상태 코드 검증
}
@Test
// 리턴값이 void 인 곳은 gpt 를 사용하였습니다.
// 새벽에 하느라 튜터님들이 안계셔서 사용한것입니다.
void User_비밀번호_변경_전체_커버리지() throws Exception {
// given
long userId = 1L;
String email = "test@test.com";
// 비밀번호 변경 요청 객체 생성
UserChangePasswordRequest passwordRequest = new UserChangePasswordRequest("oldPassword", "newPassword");
// when
doNothing().when(userService).changePassword(eq(userId), any(UserChangePasswordRequest.class));
// when & then
MvcResult mvcResult = mockMvc.perform(put("/users")
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(passwordRequest))
// AuthUserArgumentResolver가 기대하는 속성들을 설정합니다.
.requestAttr("userId", userId)
.requestAttr("email", email)
.requestAttr("userRole", UserRole.USER.name()))
.andExpect(status().isOk())
.andReturn();
// then: userService.changePassword가 올바른 인자로 호출되었는지 검증
verify(userService).changePassword(eq(userId), refEq(passwordRequest));
// then (ResponseEntity 검증)
MockHttpServletResponse response = mvcResult.getResponse();
assertEquals(HttpStatus.OK.value(), response.getStatus()); // HTTP 상태 코드 검증
}
}
무엇인가 많지만 하나의 메소드씩 분리를 해서 살펴보도록 하겠다.
@Test
void User_단일_조회() throws Exception {
//given
long userId = 1L;
String email = "test@test.com";
UserResponse userResponse = new UserResponse(userId, email);
given(userService.getUser(userId)).willReturn(userResponse);
// when && then
MvcResult mvcResult = mockMvc.perform(get("/users/{userId}", userId))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(userId))
.andExpect(jsonPath("$.email").value(email))
.andReturn();
// then (ResponseEntity 검증)
MockHttpServletResponse response = mvcResult.getResponse();
assertEquals(HttpStatus.OK.value(), response.getStatus()); // HTTP 상태 코드 검증
}
일단 컨트롤러 단독으로 테스트를 하는 것이기 때문에 userService에서 어떠한 값이 올지 미리 정의를 해주어야한다.
그 소스 코드가 given 이다.
import static org.mockito.BDDMockito.given;
위에 import를 해야 사용을 할 수 있다.
해당 소스코드는 given , when, then 패턴을 사용하였다.
given : 기본적으로 갖추어야할 조건 ( 조건 )
when : 언제 실행을 했을 때 ( 상황 )
then : 해당 결과값이 나올 것이라 추정 ( 결과 )
given 은 미리 값을 정의를 해두는 것이라고 생각을 하면된다.
userService.getUser(userId) 를 실행을 했을 경우
userRepsonse값을 돌려줄것이다. 라고 정의를 해두는 것이다.
그러면 이제 실행을 하게 된다.
mockMvc로 URI와 PathVariable 값을 받게 된다.
그러고 andExpect( Expect : 예상하다 ) 로 값을 예상을 하게되는데,
HttpStatus 는 200
body값 안에 id와 email 값이 들어갈 것이라 예상을 한것이다.
mvcResult 는 값을 가져오는데 사용이 된것이고,
가장 중요한 assertEquals로 값을 비교를 하게 된다.
원하는 값이 HttpStatus.OK.value() 값이 나올 것이라 예상을 하였다.
값이 통과가 된 것을 확인을 할 수 있다.
문제점 및 아쉬운 점
1. 준비 과정이 여러군대에서 실행이 됨
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void User_단일_조회() throws Exception {
//given
long userId = 1L;
String email = "test@test.com";
UserResponse userResponse = new UserResponse(userId, email);
given(userService.getUser(userId)).willReturn(userResponse);
// when && then 생략
}
@Test
// 리턴값이 void 인 곳은 gpt 를 사용하였습니다.
// 새벽에 하느라 튜터님들이 안계셔서 사용한것입니다.
void User_비밀번호_변경_전체_커버리지() throws Exception {
// given
long userId = 1L;
String email = "test@test.com";
// 비밀번호 변경 요청 객체 생성
UserChangePasswordRequest passwordRequest = new UserChangePasswordRequest("oldPassword", "newPassword");
// when && then 생략
}
}
단순하게만 봐도 중복으로 생성되고 사용이 되는 부분이 정말 많았다.
찾아보니 @BeforeEach로 각각의 준비 과정을 따로 준비를 할 수 있었는데, 생각을 못하고 적용을 하지 못하였다.
2. return 값이 void 인 경우는 어떻게 처리를 할 지 찾지를 못하였음.
@Test
void User를_아이디로_UserRole을_변경할_수_있다() {
// given
Long userId = 1L;
UserRoleChangeRequest userRoleChangeRequest = new UserRoleChangeRequest("ADMIN");
User user = new User();
ReflectionTestUtils.setField(user, "id", userId);
given(userRepository.findById(anyLong())).willReturn(Optional.of(user));
// when
userAdminService.changeUserRole(userId, userRoleChangeRequest);
assertEquals(user.getUserRole(), UserRole.ADMIN);
}
단순하게 Service로직 테스트를 하는 경우는 userRepository를 가지고 있는 경우가 있기 때문에
Service로직을 실행을 하고 해당 값을 직접 들고와서 사용하는 방식으로 해결을 할 수 있었다.
@Test
void changeUserRole를_호출한다() throws Exception {
// given
long userId = 1L;
long userId2 = 2L;
String email = "test@test.com";
User user = new User(email, "password", UserRole.ADMIN);
ReflectionTestUtils.setField(user, "id", userId);
User user2 = new User(email, "password", UserRole.USER);
ReflectionTestUtils.setField(user2, "id", userId2);
UserRoleChangeRequest userRoleChangeRequest = new UserRoleChangeRequest(UserRole.ADMIN.name());
given(userRepository.findById(eq(userId2))).willReturn(Optional.of(user2));
willDoNothing().given(userAdminService).changeUserRole(eq(userId2), any(UserRoleChangeRequest.class));
// when & then
mockMvc.perform(patch("/admin/users/{userId}", userId2) // <- 여기서 요청 URL 확인
.contentType(MediaType.APPLICATION_JSON)
.content(new ObjectMapper().writeValueAsString(userRoleChangeRequest))
.requestAttr("userId", userId)
.requestAttr("email", email)
.requestAttr("userRole", UserRole.ADMIN.name()))
.andExpect(status().isOk());
verify(userAdminService, times(1)).changeUserRole(eq(userId2), any(UserRoleChangeRequest.class));
}
하지만 컨트롤러의 리턴 값이 void이면서, Service 로직도 리턴값이 void면 어떻게 처리할지에 대해서
우리 모두의 친구인 chatGPT를 통해서 코드는 알 수 있었지만
아직도 이게 무슨 코드인지 verify부분을 아직도 모른다.
추가적으로 Spring 내부 기능인 Filter와 Interceptor 부분도 단위 테스트에 들어가는데,
테스트 방법을 찾지도, 방법도 몰라서 후순위로 미뤄서 진행을 하지 못하였다.
느낀점
테스트 코드를 작성을 하면서 많은 것을 느꼈다.
일단 GPT 및 생성형 인공지능을 너무 믿으면 안된다는 점
테스트 코드에 대해서 더 많이 찾아봐야겠다는 점
이번 프로젝트에 TDD(테스트 주도 개발)를 적용을 하려고 했지만,
이번에 테스트 코드 과제로 대차게 까여보면서 아직까지는 먼 이야기라는 것을 알았다.
한참 코드 독해력이 많이 올라가서 자신감을 가지게 되었지만,
다시 정상적인 시선으로 코드를 바라볼 수 있게 된 것 같다.
'Backend > Spring' 카테고리의 다른 글
[Spring] 트러블 슈팅 @RequestParam name없으면 생기는 문제 (0) | 2025.03.21 |
---|---|
[Spring] OutSourcing 프로젝트를 마치며.. (1) | 2025.03.07 |
[Spring] Filter와 Interceptor에 대해서 알아보고 사용해보자. (0) | 2025.02.26 |
[Spring] Spring 심화 내용 배운 것 단순 요약. (0) | 2025.02.21 |
[Spring] 프로젝트 하면서 느낀 아쉬운점 & 배운점 등 (0) | 2025.02.20 |