스니핏은 코드의 일부 조각을 의미하며 여기서는 문서의 일부 조각을 의미한다. 테스트 케이스 하나당 하나의 스니핏이 생성되며, 여러개의 스니핏을 모아 하나의 API 문서를 생성할 수 있다.
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id "org.asciidoctor.jvm.convert" version "3.3.2" // 추가
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
// 추가
ext {
set('snippetsDir', file("build/generated-snippets"))
}
// 추가
configurations {
asciidoctorExtensions
}
dependencies {
// 추가
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
// 추가
asciidoctorExtensions 'org.springframework.restdocs:spring-restdocs-asciidoctor'
...
}
// 추가
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
}
// 추가
tasks.named('asciidoctor') {
configurations "asciidoctorExtensions"
inputs.dir snippetsDir
dependsOn test
}
// 추가
task copyDocument(type: Copy) {
dependsOn asciidoctor
from file("${asciidoctor.outputDir}")
into file("src/main/resources/static/docs")
}
build {
dependsOn copyDocument // 추가
}
// 추가
bootJar {
dependsOn copyDocument
from ("${asciidoctor.outputDir}") {
into 'static/docs'
}
}
index.adoc
을 생성해 준다.@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class)
public @interface WithMockCustomUser {
long memberId() default 1L;
String email() default "hgd@gmail.com";
String name() default "hgd";
String password() default "1234";
String role() default "ROLE_USER";
String provider() default "local";
}
@WithMockUser(username = "test", roles = {"USER", "ADMIN"})
와 같이 일반적으로 인증이 필요한 부분에 MockUser를 생성하여 test를 진행할 메서드나 클래스에 사용할 수 있다. 하지만 이는 커스텀된 Authentication 인증 정보는 사용할 수 없다.
좀 더 유연하게 Authentication 인증 정보를사욯라기 위해 @WithSecurityContext
어노테이션을 붙여 사용한다.
public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> {
@Override
public SecurityContext createSecurityContext(WithMockCustomUser customUser) {
SecurityContext context = SecurityContextHolder.createEmptyContext();
Member member = getMember(customUser);
MemberDetails principal = MemberDetails.create(member);
// 인증에 사용하는 클래스 이용
Authentication auth =
new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities());
context.setAuthentication(auth);
return context;
}
private Member getMember(WithMockCustomUser customUser) {
Member member = new Member();
member.setMemberId(customUser.memberId());
member.setEmail(customUser.email());
member.setPassword(customUser.password());
member.setName(customUser.name());
member.setProvider(AuthProvider.valueOf(customUser.provider()));
member.setRole(customUser.role());
return member;
}
}
SecurityContext를 커스텀하게 생성하여 사용할 수 있도록 한다.
import org.springframework.restdocs.operation.preprocess.OperationRequestPreprocessor;
import org.springframework.restdocs.operation.preprocess.OperationResponsePreprocessor;
import static org.springframework.restdocs.operation.preprocess.Preprocessors.*;
public interface ApiDocumentUtils {
static OperationRequestPreprocessor getRequestPreProcessor() {
return preprocessRequest(prettyPrint());
}
static OperationResponsePreprocessor getResponsePreProcessor() {
return preprocessResponse(prettyPrint());
}
}
document 작성시 getRequestPreProcessor(), getResponsePreProcessor()를 사용하여 Request & Response에 사용되는 json을 조금 더 이쁘게 출력할 수 있다.
@Test
@DisplayName("QR 코드 생성 테스트")
public void createQrCode() throws Exception {
// given
long businessId = 1L;
QrCodeRequestDto.CreateQrCodeDto createQrCodeDto = QrCodeStubData.createQrCodeDto(businessId);
QrCode qrCode = QrCodeStubData.qrCode();
QrCodeResponseDto.QrCodeInfoDto qrCodeInfoDto = QrCodeStubData.createQrCodeInfoDto(qrCode);
given(mapper.createQrCodeDtoToQrCode(Mockito.any(QrCodeRequestDto.CreateQrCodeDto.class))).willReturn(new QrCode());
given(qrCodeService.createQrCode(Mockito.any(QrCode.class))).willReturn(qrCode);
given(mapper.qrCodeToQrCodeInfoDto(Mockito.any(QrCode.class))).willReturn(qrCodeInfoDto);
// when
ResultActions actions = mockMvc.perform(
post("/api/v1/business/{business-id}/type/reservation/qr-code", businessId)
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer {ACCESS_TOKEN}")
.content(objectMapper.writeValueAsString(createQrCodeDto))
);
// then
actions
.andExpect(status().isCreated())
.andExpect(jsonPath("$.data.qrCodeId").value(qrCodeInfoDto.getQrCodeId()))
.andExpect(jsonPath("$.data.qrCodeImg").value(qrCodeInfoDto.getQrCodeImg()))
.andExpect(jsonPath("$.data.target").value(qrCodeInfoDto.getTarget()))
.andExpect(jsonPath("$.data.qrType").value(qrCodeInfoDto.getQrType().toString()))
.andExpect(jsonPath("$.message").value("CREATED"))
.andDo(
document(
"qr-code-create",
getRequestPreProcessor(),
getResponsePreProcessor(),
requestHeaders(headerWithName("Authorization").description("Bearer AccessToken")),
pathParameters(
parameterWithName("business-id").description("매장 식별자")
),
requestFields(
List.of(
fieldWithPath("memberId").type(JsonFieldType.NUMBER).description("회원 식별자").ignored(),
fieldWithPath("target").type(JsonFieldType.STRING).description("관리 대상"),
fieldWithPath("qrType").type(JsonFieldType.STRING).description("QR 코드 타입"),
fieldWithPath("businessId").type(JsonFieldType.NUMBER).description("매장 식별자").ignored(),
fieldWithPath("dueDate").type(JsonFieldType.STRING).description("QR 코드 만료 기간")
)
),
responseFields(
List.of(
fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
fieldWithPath("data.qrCodeId").type(JsonFieldType.NUMBER).description("QR 코드 식별자"),
fieldWithPath("data.qrCodeImg").type(JsonFieldType.STRING).description("QR 코드 이미지").optional(),
fieldWithPath("data.target").type(JsonFieldType.STRING).description("관리 대상"),
fieldWithPath("data.qrType").type(JsonFieldType.STRING).description("QR 코드 타입"),
fieldWithPath("message").type(JsonFieldType.STRING).description("결과 메시지")
)
)
)
);
}
given 부분에는 request & response의 MockData를 만들어 준다.
when 부분에는 MockMvc의 perform() 메서드를 이용하여 request를 전송한다. 상황에 맞게 http method를 사용하면 된다.
then 부분에는 response의 검증 및 API 스펙 정보를 document에 추가한다.
@Test
@DisplayName("QR 코드 변경 테스트")
public void updateQrCodeTest() throws Exception {
// given
long businessId = 1L;
long qrCodeId = 1L;
QrCodeRequestDto.UpdateQrCodeDto updateQrCodeDto = QrCodeStubData.updateQrCodeDto();
QrCode updatedQrCode = QrCodeStubData.updatedQrCode();
QrCodeResponseDto.QrCodeInfoDto qrCodeInfoDto = QrCodeStubData.createQrCodeInfoDto(updatedQrCode);
MockMultipartFile dataJson = new MockMultipartFile("data", null,
"application/json", objectMapper.writeValueAsString(updateQrCodeDto).getBytes());
MockMultipartFile fileData = new MockMultipartFile("file", "qr-code.png", "image/png",
"qr-code".getBytes());
given(mapper.updateQrCodeDtoToQrCode(Mockito.any(QrCodeRequestDto.UpdateQrCodeDto.class))).willReturn(new QrCode());
given(qrCodeService.updateQrCode(Mockito.any(QrCode.class), Mockito.any(MultipartFile.class))).willReturn(updatedQrCode);
given(mapper.qrCodeToQrCodeInfoDto(Mockito.any(QrCode.class))).willReturn(qrCodeInfoDto);
// when
ResultActions actions = mockMvc.perform(
multipart("/api/v1/business/{business-id}/type/reservation/qr-code/{qr-code-id}/update", businessId, qrCodeId)
.file(dataJson)
.file(fileData)
.accept(MediaType.APPLICATION_JSON, MediaType.MULTIPART_FORM_DATA)
.header("Authorization", "Bearer {ACCESS_TOKEN}")
);
// then
actions
.andExpect(status().isOk())
.andExpect(jsonPath("$.data.qrCodeId").value(qrCodeInfoDto.getQrCodeId()))
.andExpect(jsonPath("$.data.qrCodeImg").value(qrCodeInfoDto.getQrCodeImg()))
.andExpect(jsonPath("$.data.target").value(qrCodeInfoDto.getTarget()))
.andExpect(jsonPath("$.data.qrType").value(qrCodeInfoDto.getQrType().toString()))
.andExpect(jsonPath("$.message").value("SUCCESS"))
.andDo(
document(
"qr-code-update",
getRequestPreProcessor(),
getResponsePreProcessor(),
requestHeaders(headerWithName("Authorization").description("Bearer AccessToken")),
pathParameters(
parameterWithName("business-id").description("매장 식별자"),
parameterWithName("qr-code-id").description("QR 코드 식별자")
),
requestParts(
partWithName("data").description("QR 코드 업데이트 정보").optional(),
partWithName("file").description("QR 코드 이미지 파일").optional()
),
requestPartFields("data", List.of(
fieldWithPath("qrCodeId").description("QR 코드 식별자").ignored(),
fieldWithPath("memberId").description("회원 식별자").ignored(),
fieldWithPath("target").description("관리 대상").optional(),
fieldWithPath("qrType").description("QR 코드 타입"),
fieldWithPath("dueDate").description("QR 코드 만료 기간").optional(),
fieldWithPath("businessId").description("매장 식별자").ignored()
)),
responseFields(
List.of(
fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
fieldWithPath("data.qrCodeId").type(JsonFieldType.NUMBER).description("QR 코드 식별자"),
fieldWithPath("data.qrCodeImg").type(JsonFieldType.STRING).description("QR 코드 이미지").optional(),
fieldWithPath("data.target").type(JsonFieldType.STRING).description("관리 대상"),
fieldWithPath("data.qrType").type(JsonFieldType.STRING).description("QR 코드 타입"),
fieldWithPath("message").type(JsonFieldType.STRING).description("결과 메시지")
)
)
)
);
}
MockMultipartFile 객체를 이용하여 form 형식으로 들어오는 데이터에 대한 MockData를 만들어준다.