오늘은 미리 작성된 테스트 코드를 보고 잘못된 부분을 수정하는 연습을 했다.
첫 번째 문제는 matches_메서드가_정상적으로_동작한다()
테스트가 의도대로 성공할 수 있게 수정해야 한다.
@ExtendWith(SpringExtension.class)
class PasswordEncoderTest {
@InjectMocks
private PasswordEncoder passwordEncoder;
@Test
void matches_메서드가_정상적으로_동작한다() {
// given
String rawPassword = "testPassword";
String encodedPassword = passwordEncoder.encode(rawPassword);
// when
boolean matches = passwordEncoder.matches(encodedPassword, rawPassword);
// then
assertTrue(matches);
}
}
그냥 실행하면 다음과 같이 에러가 난다. true
가 나올 것이라고 예상했지만 false
가 나왔기 때문이다.
PasswordEncoder
클래스에서 matches()
메서드를 보고 rawPassword
인자가 두 번째가 아닌 첫 번째 자리에 와야하는 것을 확인했다.
@Component
public class PasswordEncoder {
public String encode(String rawPassword) {
return BCrypt.withDefaults().hashToString(BCrypt.MIN_COST, rawPassword.toCharArray());
}
public boolean matches(String rawPassword, String encodedPassword) {
BCrypt.Result result = BCrypt.verifyer().verify(rawPassword.toCharArray(), encodedPassword);
return result.verified;
}
}
따라서 아주 간단하게 위치를 바꿔주면 끝!
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
다음은 ManagerServiceTest
에서 manager_목록_조회_시_Todo가_없다면_NPE_에러를_던진다()
테스트의 테스트 코드와 메서드 명을 수정해야 한다. 일단 여기서부터 InvalidRequestException
을 던지고 있는데 메서드 명이 맞지 않다.
@Test
public void manager_목록_조회_시_Todo가_없다면_NPE_에러를_던진다() {
// given
long todoId = 1L;
given(todoRepository.findById(todoId)).willReturn(Optional.empty());
// when & then
InvalidRequestException exception = assertThrows(
InvalidRequestException.class,
() -> managerService.getManagers(todoId)
);
assertEquals("Manager not found", exception.getMessage());
}
실행해보니 다음과 같이 에러가 난다. 메세지를 보니 "Manager not found"라는 NPE
에러가 날 것이라고 예상했지만 실제로는 "Todo not found"가 나왔기 때문이다.
ManagerService
클래스에서 매니저의 목록을 조회하는 getManagers()
메서드를 보니, Todo가 없는 경우 NPE
에러가 아닌 InvalidRequestException
이 발생한다. 따라서 메서드 명을 manager_목록_조회_시_Todo가_없다면_InvalidRequestException을_던진다()
로 변경했다.
@Transactional(readOnly = true)
public List<ManagerResponse> getManagers(long todoId) {
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
List<Manager> managerList = managerRepository.findByTodoIdWithUser(todo.getId());
return managerList.stream()
.map(manager -> new ManagerResponse(
manager.getId(),
new UserResponse(manager.getUser().getId(), manager.getUser().getEmail())
))
.collect(Collectors.toList());
}
그리고 해당 에러가 발생할 때의 메세지 역시 "Manager not found"에서 "Todo not found"로 수정한다. 그리고 다시 실행하니 성공적으로 테스트가 수행된 것을 볼 수 있다.
comment_등록_중_할일을_찾지_못해_에러가_발생한다()
테스트가 성공하도록 수정해야 한다.
@Test
public void comment_등록_중_할일을_찾지_못해_에러가_발생한다() {
// given
long todoId = 1L;
CommentSaveRequest request = new CommentSaveRequest("contents");
AuthUser authUser = new AuthUser(1L, "email", UserRole.USER);
given(todoRepository.findById(anyLong())).willReturn(Optional.empty());
// when
ServerException exception = assertThrows(
ServerException.class,
() -> commentService.saveComment(authUser, todoId, request)
);
// then
assertEquals("Todo not found", exception.getMessage());
}
실행하니 다음과 같은 에러가 발생한다. 읽어보면 "예상치못한 예외 타입인데?" 라는 의미다. ServerException
예외여야 하는데 InvalidRequestException
예외가 나왔다는 것이다.
CommentService
클래스의 saveComment()
메서드를 보자. 할일을 찾지 못하면 InvalidRequestException
예외를 던지고 있는 것을 확인할 수 있다.
@Transactional
public CommentSaveResponse saveComment(AuthUser authUser, long todoId, CommentSaveRequest commentSaveRequest) {
User user = User.fromAuthUser(authUser);
Todo todo = todoRepository.findById(todoId)
.orElseThrow(() -> new InvalidRequestException("Todo not found"));
Comment newComment = new Comment(
commentSaveRequest.getContents(),
user,
todo
);
Comment savedComment = commentRepository.save(newComment);
return new CommentSaveResponse(
savedComment.getId(),
savedComment.getContents(),
new UserResponse(user.getId(), user.getEmail())
);
}
따라서 이렇게 예외 타입을 바꿔줬다.
InvalidRequestException exception = assertThrows(
InvalidRequestException.class,
() -> commentService.saveComment(authUser, todoId, request)
);
잘 실행된다.
마지막 문제는 테스트 코드가 아닌 서비스 로직을 수정해 테스트가 통과하도록 해야한다. 테스트 코드를 보면 todo의 유저가 null인 경우 InvalidRequestException
예외가 발생하고 있다.
@Test
void todo의_user가_null인_경우_예외가_발생한다() {
// given
AuthUser authUser = new AuthUser(1L, "a@a.com", UserRole.USER);
long todoId = 1L;
long managerUserId = 2L;
Todo todo = new Todo();
ReflectionTestUtils.setField(todo, "user", null);
ManagerSaveRequest managerSaveRequest = new ManagerSaveRequest(managerUserId);
given(todoRepository.findById(todoId)).willReturn(Optional.of(todo));
// when & then
InvalidRequestException exception = assertThrows(
InvalidRequestException.class,
() -> managerService.saveManager(authUser, todoId, managerSaveRequest)
);
assertEquals("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.", exception.getMessage());
}
ManagerService
클래스에서 테스트 코드와 같은 메세지를 출력하는 로직을 살펴보니 다음과 같았다.
if (!ObjectUtils.nullSafeEquals(user.getId(), todo.getUser().getId())) {
throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
}
ObjectUtils.nullSafeEquals()
: 두 개의 객체를 비교할 때 null 안정성 보장. 일반적인 equals() 메서드와 달리 한 쪽이 true라면 예외를 발생시키지 않는다. ObjectUtils.nullSafeEquals(null, "test"); // false
ObjectUtils.nullSafeEquals(null, null); // true
그런데 테스트 코드에서는 todo의 유저가 null인 경우 예외를 발생시키고 있으므로, todo의 유저만 null이어도 예외가 발생하도록 수정했다.
if (todo.getUser() == null) {
throw new InvalidRequestException("담당자를 등록하려고 하는 유저가 일정을 만든 유저가 유효하지 않습니다.");
}
+) N+1 문제 눈으로 확인하기👀
오늘 팀원분께서 N+1 문제를 해결하지 않았을 때의 결과와 해결했을 때의 결과를 직접 보여주셨다. 그래서 나도 따라서 테스트 해보니 훨씬 이해가 잘됐다. 미리 유저 2명을 생성하고, 일정도 각 유저마다 1개씩 만들어 테스트 했더니 다음과 같은 결과가 나왔다.
@EntityGraph 적용 전 | @EntityGraph 적용 후 |
---|---|
![]() | ![]() |
N+1 문제를 해결하기 전에는 일정을 조회하고, 각 유저마다 한 번씩 더 조회하고 있다. 유저의 수(2명)만큼 쿼리가 실행된 것이다. N+1 문제를 해결한 후에는 유저를 추가로 조회하는 것 없이 쿼리가 한 번만 실행되는 것을 볼 수 있다.
오늘은 늦잠을 자서 지각을 했지만(...) 그 업보를 청산하기 위해 열심히 달렸다. 잘 자서 그런지 집중도 잘되고..😓 어제보단 나은 하루였다. 사실 과제를 오늘 거의 끝내고 내일 마무리하는 방향으로 가려고 했는데, 공부한 내용을 정리하다보니 시간이 다 갔다. 내일 더 불태워야할 것 같다.