Today I Learned
#Service 테스트
1. `@ExtendWith(MockitoExtension.class)` — 순수 단위 테스트
Spring 컨텍스트를 로딩하지 않고, Mockito만으로 서비스 레이어를 테스트하는 방식이다.
서비스가 의존하는 Repository, 외부 API, 다른 서비스 등을 전부 `@Mock`으로 대체하고, `@InjectMocks`로 테스트 대상 서비스에 주입한다.
@ExtendWith(MockitoExtension.class)
public class CommentServiceTest {
@InjectMocks
private CommentService commentService;
@Mock
private CommentRepository commentRepository;
@Mock
private CommentMapper commentMapper;
장점: 실행 속도 매우 빠름, 비즈니스 로직 집중 검증 가능, 불필요한 Bean 로딩 없음
단점: Spring Bean 주입/설정이 필요한 경우 직접 세팅해야 함, Mock 설정과 실제 호출 인자가 불일치하면 `PotentialStubbingProblem` 발생 가능
2. @SpringBootTest + @MockBean — 서비스 단위 테스트 (Spring 환경)
Spring 컨텍스트를 로딩하되, 하위 레이어는 Mock으로 대체한다.
@SpringBootTest
class CommentServiceSpringTest {
@Autowired
private CommentService commentService;
@MockBean
private CommentRepository commentRepository;
@Test
void 댓글_생성_성공() {
when(commentRepository.save(any(Comment.class)))
.thenReturn(new Comment("내용"));
Comment saved = commentService.createComment("내용");
assertThat(saved.getContent()).isEqualTo("내용");
}
}
장점: Spring 환경에서 Bean 주입 그대로 사용 가능
단점: 순수 Mockito보다 느림
3. 댓글 생성 테스트
@Test
void createComment_댓글_생성_성공() {
// given
Long taskId = 1L;
Long userId = 2L;
CommentRequest request = new CommentRequest("댓글 내용", null);
User user = UserFixture.createUser();
Task task = TaskFixture.createTask(user);
Comment comment = CommentFixture.createComment(request.content(), task, user);
UserResponse userResponse = UserFixture.createUserResponse(user, userId);
CommentResponse commentResponse = CommentFixture
.createCommentResponse(100L, "댓글 내용", taskId, userId, userResponse);
when(internalTaskService.findByTaskId(taskId)).thenReturn(task);
when(internalUserService.findByUserId(userId)).thenReturn(user);
when(commentRepository.save(any(Comment.class))).thenReturn(comment);
when(commentMapper.toCommentResponse(any(Comment.class))).thenReturn(commentResponse);
// when
CommentResponse response = commentService.createComment(taskId, request, userId);
// then
assertNotNull(response);
assertEquals("댓글 내용", response.content());
assertEquals("김철수", response.user().name());
}
모든 테스트를 Given-When-Then 패턴을 지켜 작성했다. 해당 패턴을 지키면 가독성이 좋아지고, 실패 시 원인 파익이 쉬워진다.
테스트 코드를 작성하는 과정에서 생성해줘야할 객체가 너무 많아져서 도메인별로 `CommentFixture`와 같은 클래스를 만들어서 그때그때 필요한 값을 넣어서 만들 수 있도록 했다. 중복코드를 줄이고 가독성이 향상된 것을 볼 수 있다.
@Test
void createComment_부모_댓글이_존재하지만_다른_작업에_속한_경우() {
// given
Long taskId = 1L;
Long userId = 2L;
Long parentId = 3L;
CommentRequest request = new CommentRequest("대댓글 내용", parentId);
User user = UserFixture.createUser();
Task task = TaskFixture.createTask(user);
Task otherTask = TaskFixture.createTask(user);
ReflectionTestUtils.setField(otherTask, "taskId", 20L);
Comment parentComment = CommentFixture.createComment(request.content(), otherTask, user);
when(internalTaskService.findByTaskId(taskId)).thenReturn(task);
when(commentRepository.findById(parentId)).thenReturn(Optional.of(parentComment));
// when & then
assertThrows(CommentException.class, () -> commentService.createComment(taskId, request, userId));
}
원하는 시나리오를 위해 `otherTask`는 다른 작업이기에 ID 비교가 필요함으로 `ReflectionTestUtils.setField()`를 활용해서 ID값을 강제로 넣어주었다.
마치며
PR 리뷰 과정에서 팀원들의 피드백을 반영해 코드를 수정했는데, 머지 직전에야 테스트를 돌리지 않았다는 사실을 깨달았다. 코드를 조금이라도 수정했다면 반드시 즉시 테스트를 실행해야 한다는 점을 다시 한 번 느꼈다. 또한 테스트 작성 과정에서 Task 존재 여부나 작성자인 User 존재 여부를 검증하는 코드를 추가했었는데, 해당 검증은 이미 각 도메인 단위에서 충분히 테스트되고 있었기에 중복된 테스트라는 것을 뒤늦게 알게 되었다. 결국, 테스트 시나리오를 설계할 때는 중복을 피하면서도 다양한 관점에서 케이스를 고민하는 것이 중요하다는 점을 배우는 시간이었다.