Spring RestDocs

박윤택·2022년 10월 9일
1

Spring Rest Docs 적용

Spring Rest Docs API 문서 생성 흐름

스니핏은 코드의 일부 조각을 의미하며 여기서는 문서의 일부 조각을 의미한다. 테스트 케이스 하나당 하나의 스니핏이 생성되며, 여러개의 스니핏을 모아 하나의 API 문서를 생성할 수 있다.


Spring RestDocs 설정

  • build.gradle 설정
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'     
	}
}
  • API 문서 스니핏을 사용하기 위한 템플릿 API 문서 생성
    src/docs/asciidoc/ 디렉토리 내에 비어있는 템플릿 문서 index.adoc을 생성해 준다.

Spring RestDocs 예시

인증,인가 - MockUser 사용

  • WithMockCustomUser 어노테이션 생성
@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 어노테이션을 붙여 사용한다.


  • SecurityContext 커스터마이즈
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를 커스텀하게 생성하여 사용할 수 있도록 한다.


Request & Response 이쁘게 출력하기

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을 조금 더 이쁘게 출력할 수 있다.


CRUD

  @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에 추가한다.


From 형식의 Request를 받을 때

  @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를 만들어준다.


0개의 댓글