[Mockito] Mock 객체를 사용하여 테스트 코드 작성 + Spring Rest Docs 적용
⚠️ 이 글은 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()
만 호출했는지 검증한다.
만약, userRepository
가 findById
라는 다른 함수와도 상호작용을 했다면 이 검증은 실패한다.
여러가지 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
이 글을 참고했다.
- 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
파일을 기준으로 생성됩니다.
- html 문서가 생성되는 기준은
Spring Rest Docs 적용
위처럼 테스트를 작성한 뒤, 테스트에 성공하면 build/generated-snippets
경로에 .adoc
파일들이 생성된다.
생성된 스니펫들을 사용해보자!
이 글을 참고했다.
.adoc
파일을 생성해야 하는데, Gradle의 경우 src/docs/asciidoc/*.adoc
경로로 생성해야 한다.
파일명은 자유롭게 설정하면 된다. (.adoc
으로 생성하기만 하면 OK)
그리고 스니펫들을 사용하는.. 진짜 문서로서의 기능을 하도록 조합하면 끝!
=
,==
… 들은 마크다운에서의#
,##
과 비슷하게 사용.. 제목이 된다:toc: left
설정은 왼쪽에 사이드바가 생성되고, 목차들에 링크가 자동으로 걸린다!!:toclevels: 2
설정은 목차들의 레벨을 어디까지 보여줄 것이냐..
= REST API
:toc: left
:toclevels: 2
:source-highlighter: highlightjs
== 1. 개요
== 2. 인증
== 3. 이미지
이제 생성된 스니펫들을 진짜로 포함해보자!
includ::{snippets}/../~.adoc[]
으로 사용할 스니펫을 선택해서 불러오면 된다!
인텔리제이를 사용한다면 플러그인을 다운받아 작성하시길… 문법이 바로바로 이해가 쏙쏙…
== 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를 더욱 최적화 하고 싶다면 이 글을 참고하자.
💛 개인 공부 기록용 블로그입니다. 👻