프로젝트/기술 면접 복습 플랫폼

단위 테스트 + API 문서화(MockMvc, RestDocs)

yoon4360 2025. 4. 25. 19:54

 

프로젝트를 진행하면서 테스트는 해야겠는데, 어디까지 어떻게 해야 하지? 라는 고민이 앞섰다.
그리고 API 문서는 RestDocs가 낫나, Swagger가 낫나? 라는 질문에는

둘 다 해보지 각 문서의 장단점을 비교해 보자는 생각을 했다.

이번 글에서는 DevInterview 프로젝트를 진행하며,
단위 테스트 → API 문서화 흐름을 어떻게 설계했는지 정리해보려 한다.

 


 

단위 테스트 – 서비스 테스트

 

목적: 서비스 메서드가 정상 동작하는지 검증하는 것이다.

 

auth, qna, writing, review, user 여러가지 도메인이 있지만 그 중 가장 중요하고 핵심이라고 생각되는 

인증 도메인인 auth와 면접 질문을 관리하는 qna 도메인을 위주로 설명해보겠다.

 

1. AuthService 테스트

테스트 포인트

  • 로그인 성공 시, 유저 인증 후 토큰 발급되는가?
  • 비밀번호 불일치 시 예외 발생하는가?
  • 존재하지 않는 이메일일 때 예외 발생하는가?
  • 리프레시 토큰 재발급 로직이 정상 동작하는가?
  • 리프레시 토큰 불일치/만료 시 예외 발생하는가?

 

테스트 방식

  • Repository, JWT Provider 등 외부 의존성을 Mock 처리한다.
  • 정상 로직 + 다양한 예외 상황을 테스트한다.
  • 리턴되는 값, 상태 검증 + 메서드 호출 여부를 검증한다.
// 로그인 성공 테스트
@Test
    void login_success() {
        //given
        String email = "test@example.com";
        String password = "password";
        UUID userId = UUID.randomUUID();
        String encodedPassword = "encodedPassword";

        User user = User.builder()
                .id(userId)
                .email(email)
                .password(encodedPassword)
                .build();


        JwtToken token = new JwtToken("access-token", "refresh-token");

        when(userRepository.findByEmail(email)).thenReturn(Optional.of(user));
        when(passwordEncoder.matches(password, encodedPassword)).thenReturn(true);
        when(jwtTokenProvider.generateTokens(userId)).thenReturn(token);

        // when
        LoginResponse response = authService.login(new LoginRequest(email, password));

        // then
        assertEquals(userId, response.userId());
        assertEquals("access-token", response.accessToken());
        assertEquals("refresh-token", response.refreshToken());
        verify(refreshTokenRepository).save(any(RefreshToken.class));
    }

 

 

2. QnaService 테스트

 

테스트 포인트

  • 질문 저장 시 사용자/글 검증 후 정상 저장되는가?
  • 질문 삭제 시, 본인 여부 확인 후 soft delete 되는가?
  • 복습 대상 질문 리스트 정확히 조회되는가?

테스트 방식

  • UserRepository, QnaRepository를 Mock 처리한다.
  • 시간(LocalDateTime) 조건 기반 검증한다.
  • 삭제 시 권한 예외 테스트한다.
// 오늘 리뷰 qna 조회
@Test
void getReviewQnasForToday_success() {
    UUID userId = UUID.randomUUID();
    Qna qna = Qna.builder()
            .id(UUID.randomUUID())
            .scheduledDate(LocalDateTime.now().minusDays(1))
            .isDeleted(false)
            .build();

    when(qnaRepository.findAllByUserIdAndScheduledDateBeforeAndIsDeletedFalse(eq(userId), any(LocalDateTime.class)))
            .thenReturn(List.of(qna));

    List<QnaTodayResponse> result = qnaService.getReviewQnasForToday(userId);

    assertEquals(1, result.size());
    verify(qnaRepository).findAllByUserIdAndScheduledDateBeforeAndIsDeletedFalse(eq(userId), any(LocalDateTime.class));
}

 


 

단위 테스트 – 컨트롤러 테스트

 

목적: API가 외부에 잘 노출되는지 + 문서 자동화

 

1. AuthController 테스트

테스트 포인트

  • /api/auth/login 요청 → JSON 요청/응답 형식을 문서화한다.
  • /api/auth/reissue 요청 → 쿼리 파라미터 + 응답 문서화한다.

테스트 방식

  • MockMvc 사용, HTTP 요청 시나리오를 테스트한다.
  • requestFields, responseFields, queryParameters 문서화한다.
  • API 문서 생성을 위한 정적 검증 (RestDocs)한다.
// 코드 재발급 성공 테스트
@Test
@DisplayName("토큰 재발급 API 성공 테스트")
void reissue_success() throws Exception {
    // given
    String oldRefreshToken = "old-refresh-token";
    String newAccessToken = "new-access-token";
    String newRefreshToken = "new-access-token";

    JwtToken newToken = new JwtToken(newAccessToken, newRefreshToken);

    when(authService.reissue(oldRefreshToken)).thenReturn(newToken);

    // when & then
    mockMvc.perform(post("/api/auth/reissue?refreshToken="+oldRefreshToken)
                    .contentType(MediaType.APPLICATION_JSON))
            .andDo(document("reissue-success",
                    queryParameters(
                            parameterWithName("refreshToken").description("기존 리프레시 토큰")
                    ),
                    responseFields(
                            fieldWithPath("accessToken").description("새로운 엑세스 토큰"),
                            fieldWithPath("refreshToken").description("새로운 리프레시 토큰")
                    )
                    ))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.accessToken").value(newAccessToken))
            .andExpect(jsonPath("$.refreshToken").value(newRefreshToken));
}

 

QnaController 테스트

테스트 포인트

  • /api/qna/{writingId} POST → 질문 저장 API 요청/응답을 문서화한다.
  • /api/qna/today GET → 쿼리 파라미터 기반 조회를 문서화한다.
  • /api/qna/user/{userID} GET → PathVariable 문서화한다.

테스트 방식

  • MockMvc + RestDocs 조합
  • Path Parameters, Query Parameters, Response 구조 상세 문서화한다.
  • 문서화를 위한 예외 없이 성공 케이스 중심 테스트
// 질문 저장 성공 테스트
@Test
@DisplayName("GPT 질문저장 API 성공테스트")
void createQna_success() throws Exception {
    //given
    UUID writingId = UUID.randomUUID();
    UUID userId = UUID.randomUUID();
    String question = "Spring 이란?";
    String answer = "Spring 은 자바 웹 프레임워크입니다.";

    QnaCreateRequest request = new QnaCreateRequest(userId, question, answer);

    Qna saved = Qna.builder()
            .id(UUID.randomUUID())
            .user(User.builder().id(userId).build())
            .writing(Writing.builder().id(writingId).build())
            .question(question)
            .answer(answer)
            .scheduledDate(LocalDateTime.now())
            .isDeleted(false)
            .build();

    when(qnaService.saveQna(eq(userId), eq(writingId), eq(question), eq(answer), any(LocalDateTime.class)))
            .thenReturn(saved);

    // when & then
    mockMvc.perform(post("/api/qna/{writingId}", writingId)
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(request)))
            .andDo(document("qna-create-success",
                    pathParameters(
                            parameterWithName("writingId").description("작성 글 ID")
                    ),
                    requestFields(
                            fieldWithPath("userId").description("사용자 ID"),
                            fieldWithPath("question").description("질문 내용"),
                            fieldWithPath("answer").description("질문에 대한 답변")
                    ),
                    responseFields(
                            fieldWithPath("qnaId").description("저장된 QnA ID")
                    )
                    ))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.qnaId").value(saved.getId().toString()));
}

 


 

테스트 정리

 

테스트 3단계 흐름

  1. given: 테스트에 필요한 값/상태 준비한다.
  2. when: 테스트 대상 코드를 실행한다.
  3. then: 결과를 검증한다. (예상 결과 vs 실제 결과)

테스트 도구

MockMvc 가짜 HTTP 요청/응답 수행
Mockito 서비스, 레포지토리 Mocking
ObjectMapper 객체 ↔ JSON 변환

 


 

REST Docs 

 

동작 흐름

  1. 테스트 코드 안에서 .andDo(document(...)) 작성
  2. 테스트 성공 시 → generated-snippets 폴더에 adoc 파일 생성
  3. index.adoc 작성 후 asciidoctor 빌드
  4. 정적 HTML 문서 생성 (예: build/docs/asciidoc/index.html)
mockMvc.perform(post("/api/users")
    .contentType(MediaType.APPLICATION_JSON)
    .content(objectMapper.writeValueAsString(userDto)))
    .andDo(document("user-create",
        requestFields(
            fieldWithPath("email").description("이메일"),
            fieldWithPath("password").description("비밀번호")
        ),
        responseFields(
            fieldWithPath("id").description("회원 ID"),
            fieldWithPath("email").description("가입한 이메일")
        )
    ));

 

REST Docs 장점

  • 테스트 결과 기반 → 항상 정확한 문서가 만들어 진다.
  • 정적 HTML 파일 → 배포/공유에 용이하다.
  • 협업 시 → 신뢰도 높은 API 스펙을 공유할 수 있다.

 


 

Swagger

 

설정 방법

  • springdoc-openapi 라이브러리 추가
  • 컨트롤러 메서드에 @Operation, @Parameter 등 애노테이션 추가

접속 URL

  • http://localhost:8080/swagger-ui/index.html

Swagger 장점

  • 실시간 API 확인이 가능하다.
  • 개발 중 편리하게 인터페이스를 테스트 할 수 있다.
  • 자동화된 문서화 → 수정이 간편하다.

 


 

RestDocs vs Swagger 비교

구분 RestDocs Swagger
문서 생성 방식 테스트 기반 (MockMvc 필요) 애노테이션 기반 (실시간 문서화)
결과물 정적 HTML 문서 동적 웹 페이지
공유 방식 HTML 파일 배포 서버 배포 필요
유지보수 테스트 시 자동 갱신 애노테이션 수정 필요
인터랙티브 테스트 불가능 가능
정확성 테스트 기반 – 매우 정확 동적, 가끔 누락 가능

 


 

마무리

이번 테스트와 문서화는

테스트 없는 문서는 신뢰하기 어렵고, 문서 없는 테스트는 공유하기 어렵다는 것을 되새기며 열심히 설계했다.

 

RestDocs + Swagger 병행해서 두 방식을 비교 해볼 수 있어

나름 유의미 했던 노력이었던 것 같다.
외부 공유는 RestDocs, 개발 중엔 Swagger로 실시간 확인하는 것이 좋다.