ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring REST Docs 적용
    공부일기/스프링 2021. 7. 26. 18:06
    • 팀의 API 문서 자동화
    • 프로덕션 코드 작성 전 API 명세를 맞추기 위해 MockMvc 활용

    Spring REST Docs는 RESTful 서비스의 문서화를 도와주는 도구이다. Spring REST Docs는 문서 작성 도구로 기본적으로 Asciidoctor를 사용해 HTML을 생성한다. 필요한 경우 Markdown 을 사용하도록 변경할 수 있으나 편의를 위해 Asciidoctor로 작성했다.

    Spring REST Docs는 Spring MVC의 테스트 프레임 워크 또는 Rest Assured 3로 작성된 테스트 코드에서 생성된 Snippet을 사용한다.

    테스트 기반 접근 방식은 서비스 문서의 정확성을 보장해준다. Snippet이 올바르지 않을 경우 테스트가 실패하기 때문이다.

    Spring REST Docs와 마찬가지로 많이 사용되는 문서도구로는 Swagger가 있으나 아래와 같은 사항들을 비교해 사용하지 않았다.

    Spring REST Docs vs Swagger

    • Rest Docs는 Swagger에 비해 사용법은 어렵지만, Swagger 사용시 발생할 수 있는 코드 동기화 이슈를 막을 수 있다.
    • 테스트를 성공시켜야 문서 작성되기 때문에 테스트 작성을 강제할 수 있다.
    • 프로덕션 코드를 작성하고 테스트 작성 시 협업을 하고 API 명세를 작성하는 데 늦어지기 때문에 MockMvc(@WebMvcTest)를 사용한다.Spring REST Docs 적용

    MockMvc(@WebMvcTest)

    브라우저에서의 요청과 응답을 의미하는 객체로 Controller 테스트를 용이하게 해주는 라이브러리이다.

    • 컨트롤러 레이어만 테스트하기에 속도가 빠르다.
    • Rest Assured의 경우 전체 컨텍스트를 로드하고 빈을 주입하기 때문에 속도가 느리다.

     

    빌드 시스템 설정

     

    plugins { 
      	// Asciidoctor 플러그인 적용
      	id "org.asciidoctor.convert" version "1.5.9.2"
      }
    
      dependencies {
      	/* 
      		asciidocker 에 대한 spring-restdocs-asciidocker 의존성 추가
      		Maven 처럼 build/generated-snippets 밑에 생성된 Snippet 을 .adoc 파일이 자동으로
      		가리키도록 하는 설정 추가.
      		operation 블록 매크로 사용 가능
      	*/
      	asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor:{project-version}' 
      	// Maven 과 같이 test Scope 에 대한 mockMvc 의존성을 추가 (WebClient, Assured 사용가능) 
      	testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc:{project-version}' 
      }
    
      ext { 
      	// Snippet 의 생성 위치를 지정
      	snippetsDir = file('build/generated-snippets')
      }
    
      test { 
      	// Snippets 디렉토리를 출력으로 작업하도록 설정
      	outputs.dir snippetsDir
      }
    
      asciidoctor { 
      	// Snippets 디렉토리를 Input 디렉토리로 설정
      	inputs.dir snippetsDir 
      	// 문서 생성 전 테스트가 실행되도록 test 에 종속 설정
      	dependsOn test 
      }
    
    bootJar {
      	// 빌드 전 문서 생성 확인
      	dependsOn asciidoctor
      	// 생성된 문서를 static/docs 에 복사 
      	from ("${asciidoctor.outputDir}/html5") { 
      		into 'static/docs'
      	}
      }

     

    Snippet 사용

    테스트 후 생성되는 Snippet을 사용하기 위해 .adoc 파일을 생성해야 한다. 파일명은 상관없고, 생서되는 snippet을 연결해주면 된다.

    아래는 Snippet을 사용한 .adoc 파일의 일부와 html로 출력될 경우의 예시이다.

    ifndef::snippets[]
    :snippets: ../../../build/generated-snippets
    endif::[]
    :doctype: book
    :icons: font
    :source-highlighter: highlightjs
    :toc: left
    :toclevels: 2
    :author: Team CVI
    :email: https://github.com/woowacourse-teams/2021-cvi
    // 여기까진 설정 부분
    
    == User (유저)
    === 유저 가입
    ==== 성공
    ===== Request
    include::{snippets}/user-signup/http-request.adoc[]
    
    ===== Response
    include::{snippets}/user-signup/http-response.adoc[]
    
    ==== 실패
    ===== Request
    include::{snippets}/user-signup-failure/http-request.adoc[]
    
    ===== Response
    include::{snippets}/user-signup-failure/http-response.adoc[]

     

    Spring REST Docs를 위해 작성된 테스트 코드 예제

    @AutoConfigureRestDocs
    @AutoConfigureMockMvc
    public class ApiDocument {
    
        @Autowired
        protected MockMvc mockMvc;
    
        protected static OperationRequestPreprocessor getDocumentRequest() {
            return preprocessRequest(
                    modifyUris()
                            .scheme("http")
                            .host("localhost")
                            .removePort(),
                    prettyPrint());
        }
    
        protected static OperationResponsePreprocessor getDocumentResponse() {
            return preprocessResponse(prettyPrint());
        }
    
        protected static RestDocumentationResultHandler toDocument(String title) {
            return document(title, getDocumentRequest(), getDocumentResponse());
        }
    
        protected static String toJson(Object object) {
            try {
                ObjectMapper objectMapper = new ObjectMapper();
                objectMapper.registerModule(new JavaTimeModule());
                objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
                return objectMapper.writeValueAsString(object);
            } catch (Exception e) {
                throw new IllegalStateException("직렬화 오류");
            }
        }
    }
    @WebMvcTest(controllers = UserController.class)
    class UserControllerTest extends ApiDocument {
    
        @MockBean
        private UserService userService;
    
        private UserRequest userRequest;
        private UserResponse userResponse;
    
        @BeforeEach
        void init() {
            userRequest = new UserRequest("라이언", 20);
            userResponse = new UserResponse(1L, "라이언", 20);
        }
    
        @DisplayName("유저 가입 - 성공")
        @Test
        void signup() throws Exception {
            //given
            willReturn(userResponse).given(userService).signup(any(UserRequest.class));
            //when
            ResultActions response = 유저_회원가입_요청(userRequest);
            //then
            유저_회원가입_성공함(response, userResponse);
        }
    
        @DisplayName("유저 가입 - 실패")
        @Test
        void signupFailure() throws Exception {
            //given
            willThrow(new InvalidInputException("중복된 닉네임이 존재합니다.")).given(userService).signup(any(UserRequest.class));
            //when
            ResultActions response = 유저_회원가입_요청(userRequest);
            //then
            유저_회원가입_실패함(response);
        }
    }

     

    perform()

    • 내부에 컨트롤러 호출 방식인 get("호출 URI"), post("호출 URI"), put("호출 URI"), delete("호출 URI")가 들어갈 수 있다.
    • header(), accept(), contentType(), content(), param() 등을 호출할 수 있다.
    private ResultActions 유저_회원가입_요청(UserRequest request) throws Exception {
        return mockMvc.perform(post("/api/v1/users/signup")
                .contentType(MediaType.APPLICATION_JSON)
                .content(toJson(request)));
    }

     

    ResultActions

    MockMvc.perform() 메서드로 리턴되는 인터페이스. 지원 메서드로는 andExpect(), andDo(), andReturn() 등이 있다.

    private void 유저_회원가입_성공함(ResultActions response, UserResponse userResponse) throws Exception {
        response.andExpect(status().isCreated())
                .andExpect(header().string("Location", "/api/v1/users/" + 1))
                .andExpect(content().json(toJson(userResponse)))
                .andDo(print())
                .andDo(toDocument("user-signup"));
    }

     

    JsonPath

    요청 결과가 Json인 경우 jsonPath를 사용해 검증할 수 있다.

    mockMvc.perform(post("/test1")                 
           .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)     
           .content(mapper.writeValueAsString(new TestInputVO("11", "22"))))          
           .andExpect(status().isOk())                            
           .andExpect(header().string("ans_cd", "0000"))
           .andExpect(jsonPath("$.value1").value("A"))
           .andExpect(jsonPath("$.value2").isNotEmpty())
           .andExpect(jsonPath("$.value3").value("0000"));

     

     


    참고 : https://twofootdog.github.io/Spring-Spring-MVC에서-JUnit-활용하기2(MockMvc를-활용한-Controller-테스트)/

Designed by Tistory.