3 분 소요

⚠️ 이 글은 Spring Rest Docs 적용 후에 테스트 코드를 작성하는 방법 위주로 설명할 예정이다.
아직 Spring Rest Docs가 적용되어 있지 않다면, 이 글을 참고하자.

개요

Mockito란?

테스트를 편하게 하도록 모의 객체(Mock)를 만드는 Mocking 프레임워크입니다.

Mock이란?

모의 객체(Mock)는 실제 구현체가 없고, 껍데기(인터페이스, 메서드, 필드)를 참조할 수 있는 객체입니다.
외부 객체를 Mock 함으로써 테스트할 객체를 외부 객체의 실제 구현 내용과 분리해서 생각할 수 있습니다.

Mockito 사용법

Mock 객체 생성

Service Test

@ExtendWith(MockitoExtension.class)
class DiaryServiceTest {

    @Mock
    UserRepository userRepository;

    @InjectMocks
    DiaryServiceImpl diaryService;

    ...
}

Controller Test

@ExtendWith(RestDocumentationExtension.class)
@WebMvcTest(DiaryController.class)
@ActiveProfiles("test")
@MockBean(JpaMetamodelMappingContext.class)
class DiaryControllerTest {

    @MockBean // 서비스 주입 (해당 클래스를 Mock 처리하고 스프링에서 bean으로 등록하여 사용하기 위해 선언)
    private DiaryService diaryService;

    @Autowired
    private MockMvc mockMvc;

    @BeforeEach
    public void setup(WebApplicationContext webApplicationContext, RestDocumentationContextProvider restDocumentation) {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
//                .apply(springSecurity())    // springSecurity설정이 되어있지 않으면 생략
                .apply(documentationConfiguration(restDocumentation))
                .apply(sharedHttpSession())
                .build();
    }

    ...
}

⚠️ JUnit5의 경우 @AutoConfigureRestDocs 설정이 안 먹힌다..!!

테스트 작성법

이 글을 참고했다.

Stub

when().thenReturn()

when(userRepository.findByUsername("test")).thenReturn(Optional.of(user));

thenReturn()으로 단순한 값을 반환할 수 있다.

when().thenThrow()

when(contestService.modify(anyLong(), any())).thenThrow(new AlreadyContestEndException());

thenThrow()로 예외를 던지자.

when().thenAnswer()

when(contestService.modify(anyLong(), any())).thenAnswer(invocation -> {
    doSomething();
    return Optional.of(user);
});

thenAnswer()로 유연하게 Stub 할 수 있다.

검증하기

verify함수는 인자로 Mock객체를 받습니다.
그리고 해당 Mock 객체의 원하는 상호작용이 있었는가 검증합니다.
메서드가 호출되었는지, 필드를 참조했는지가 이에 해당합니다.

verify(..., times())

verify(userRepository, times(2)).findByUsername("test");

함수가 두 번 호출되었는가를 검증한다. 두 번 호출했으면 통과한다.

verify(..., never())

verify(userRepository, never()).findByUsername("test");

함수가 호출되지 않음을 검증한다. 호출되지 않았으면 통과한다.

verify(..., atLeast())

verify(userRepository, atLeast(3)).findByUsername("test");

함수가 적어도 n번 호출되었는지 검사한다. 만약 3번보다 적게 호출했다면 이 검증은 실패한다.

verify(..., atMost())

verify(userRepository, atMost(3)).findByUsername("test");

함수가 최대 n번 호출되었는지 검사한다. 만약 3번보다 많이 호출했다면 이 검증은 실패한다.

verify(..., only())

verify(userRepository, only()).findByUsername("test");

오직 “한 번”, findByUsername()만 호출했는지 검증한다.
만약, userRepositoryfindById라는 다른 함수와도 상호작용을 했다면 이 검증은 실패한다.

여러가지 Function 사용해보기

  • 테스트가 성공해야 스니펫이 생성된다.
  • @MockBean으로 주입된 서비스에 대해 적절한 처리를 해준다.. -> given().willReturn()
  • 요청, 응답에 대한 문서에 적힐 설명들을 작성
    • requestParameters는 queryString으로 요청하는 값에 대해..
    • responseFields는 응답 값에 대해.. 형식에 맞게 적어주는 것이 중요 -> 공식문서 참고
    • optional()을 붙여주면 필수 값이 아니라는 뜻 붙이지 않으면 테스트 실패하니 주의
@Test
// @WithMockUser(...) // springSecurity 설정 없으면 생략
void getImages() throws Exception {

    List<HashMap<String, Object>> response = new ArrayList<>();
    HashMap<String, Object> data = new HashMap<>();
    data.put("name", "foo");
    data.put("img_url", "https://~~~");
    data.put("reg_date", "2021-06-03 10:00:00");
    response.add(data);

    given(imageService.findAllById(any(ParamDto.class))).willReturn(response);

    ResultActions perform = this.mockMvc.perform(
            RestDocumentationRequestBuilders.get("/images")
    );

    perform.andExpect(status().isOk())
            .andDo(print())
            .andDo(document("images-get",    // 설정한 값으로 스니펫 폴더가 생성됨
                requestParameters(
                    parameterWithName("user").description("유저 이름").optional()
                ),
                responseFields(
                    fieldWithPath("imgList").description("이미지 리스트"),
                    fieldWithPath("imgList[].name").description("이미지 등록 회원 이름"),
                    fieldWithPath("imgList[].img_url").description("이미지 url"),
                    fieldWithPath("imgList[].reg_date").description("이미지 등록일시")
                })
            );

}

🤔 given().willReturn()when().thenReturn()의 차이가 궁금하다면 이 글을 참고하자.

fieldWithPath

  • API의 Request, Response Snippet을 구성하는 요소를 정의하는 가장 기본적인 function입니다.
  • fieldWithPath(“key”) 형태로 API Request, Response 내의 요소를 정의 할 수 있습니다.
  • Attributes
    • description(“내용”) : Request, Response 내의 요소의 내용을 정의합니다.
    • type(JsonFieldType.TYPE) : Request, Response 내의 요소의 타입을 정의합니다.
    • optional() : Request, Response 내의 요소의 필수값 여부를 정의합니다. (optional()를 선언하면 필수값 아님)
.andDo(document("getDetailedDiary",
                responseFields(
                        fieldWithPath("[].title").type(JsonFieldType.STRING).description("일기 제목"),
                        fieldWithPath("[].content").type(JsonFieldType.STRING).description("일기 내용")
                )))

requestHeader

  • API의 Request Header에 대한 Snippet을 생성합니다.
  • /build/generated-snippets/<document-name>/request-headers.adoc 인 파일명으로 생성됩니다.
  • requestHeader(headerWithName(“header-key명”).description(“header-key가 의미하는 내용”))
.andDo(document("getDetailedDiary",
                requestHeaders(
                  headerWithName("api-auth-key").description("API 인증 키")
                )))

PathParameters

  • API의 Path Parameter에 대한 Snippet을 생성합니다.
  • /build/generated-snippets/<document-name>/path-parameters.adoc 인 파일명으로 생성됩니다.
  • pathParameter(parameterWithName(“key 명칭”).description(“key가 의미 하는 내용”)) 으로 코드를 작성합니다.
.andDo(document("getDetailedDiary",
                pathParameters(
                        parameterWithName("diaryId").description("조회할 일기의 ID")
                )))

더 많은 Function들은 이 글을 참고히자.

Spring Rest Docs

Architecture

이 글을 참고했다.

asciidoctor

  • Test Case를 수행하면 산출물이 .adoc 파일로 /build/generate-snippets 디렉토리에 생성됩니다. (default path)
  • /src/docs/asciidoc 디렉토리에 /build/generate-snippets에 있는 adoc 파일을 include하여 문서를 생성할 수 있습니다.
    • /build/generate-snippets/*.adoc 파일들은 API Request, Response에 대한 명세들만 있는 파일이고
    • /src/docs/asciidoc/*.adoc 파일들이 실제 사용자에게 html파일로 변환되어 제공되는 API 문서 파일입니다.
    • 따라서 /src/docs/asciidoc/*.adoc 에 API 문서를 작성하고,
      필요한 API Request, Response Spec은 자동 생성된 /build/generate-snippets/*.adoc 파일들을 이용해 표현해줍니다.
    • 이렇게 하면 향후 API Spec이 변경되더라도, 문서를 수정하지 않아도 되는 장점이 있습니다.
  • 이렇게 생성된 asciidoc 문서는 AsciidoctorTask를 통해 html 문서로 processing 되어 /build/asciidoc/html5 하위에 html문서로 생성됩니다.
    • html 문서가 생성되는 기준은 /src/docs/asciidoc/*.adoc 파일을 기준으로 생성됩니다.

Spring Rest Docs 적용

위처럼 테스트를 작성한 뒤, 테스트에 성공하면 build/generated-snippets경로에 .adoc파일들이 생성된다.
생성된 스니펫들을 사용해보자!

이 글을 참고했다.

.adoc파일을 생성해야 하는데, Gradle의 경우 src/docs/asciidoc/*.adoc 경로로 생성해야 한다.
파일명은 자유롭게 설정하면 된다. (.adoc으로 생성하기만 하면 OK)
스크린샷 2023-03-21 오후 6 24 05

그리고 스니펫들을 사용하는.. 진짜 문서로서의 기능을 하도록 조합하면 끝!

  • =, == … 들은 마크다운에서의 #, ## 과 비슷하게 사용.. 제목이 된다
  • :toc: left 설정은 왼쪽에 사이드바가 생성되고, 목차들에 링크가 자동으로 걸린다!!
  • :toclevels: 2 설정은 목차들의 레벨을 어디까지 보여줄 것이냐..
= REST API
:toc: left
:toclevels: 2
:source-highlighter: highlightjs

== 1. 개요

== 2. 인증

== 3. 이미지

이제 생성된 스니펫들을 진짜로 포함해보자!
includ::{snippets}/../~.adoc[]으로 사용할 스니펫을 선택해서 불러오면 된다!

인텔리제이를 사용한다면 플러그인을 다운받아 작성하시길… 문법이 바로바로 이해가 쏙쏙…
스크린샷 2023-03-21 오후 6 26 33

== 3. 이미지
=== 3-1. 이미지 조회
===== Request
include::{snippets}/images-get/request-parameters.adoc[]
===== Request Example
include::{snippets}/images-get/http-request.adoc[]

===== Response
include::{snippets}/images-get/response-fields.adoc[]

===== Response Example
include::{snippets}/images-get/response-body.adoc[]

최적화

Spring Rest Docs를 더욱 최적화 하고 싶다면 이 글을 참고하자.



💛 개인 공부 기록용 블로그입니다. 👻

맨 위로 이동하기

태그:

카테고리:

업데이트: