한 주가 끝나고 새로운 주가 시작되었다.

image

Spring 핵심 기술을 배우는 섹션이 거의 막바지로 다가왔다.
내가 사용할 수 있는 기술은 정말 적은 것 같은데

벌써 어느덧 섹션 끝에 다다르다니…
시간이 진짜 빠르다고 느끼는 반면, 좀 더 열심히해서
지식을 채워 넣어야 내 목적지까지 도달할 수 있을 것 같다는 생각이 든다.


오늘은 Spring Rest Docs를 활용해보는 시간이다.

저번주에 간단하게 사용방법을 배웠고
오늘은 Controller의 모든 API를 문서화 시켜보려한다.

API 자동 문서화 연습

저번주에는 POST와 PACTH에 대한
자동 문서화를 진행했다.

지금 만들고 있는 애플리케이션에서는 GET, DELETE
API만 자동화 시켜주면 마무리가 된다.

import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.doNothing;
import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.*;
import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import static org.springframework.restdocs.payload.PayloadDocumentation.*; //responseFields(); , fieldWithPath() 사용
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; // document();
import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; // pathParameters();

시작 전 우리가 사용하기 위한 기술에 대한
import를 진행해준다.

유의 깊게 볼 것은 RestDocs 구성을 위한 API들이다.


@AutoConfigureRestDocs
@WebMvcTest(MemberController.class)
@MockBean(JpaMetamodelMappingContext.class)
public class DocumentationTest {
    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private MemberService memberService;

    @MockBean
    private MemberMapper mapper;

    @Autowired
    private Gson gson;
    
    ...
    
}

기본적인 위존성 주입과 Class 애노테이션을 셋팅 한 다음
Test 코드를 작성해 API 자동화하여 만들어 줄 수 있다.

@Test 코드 작성 시 우리가 이전 부터 학습해왔던
Mockito를 이용해 Slice 계층별 테스트를 만들어 줄 수가 있다.

Controller의 계층 별 테스트를 위해
Service영역과의 연결된 부분을 Mock 객체를 반환시켜
실제 데이터가 들어간 것 처럼 테스트하는 방법이다.

내가 총 만들어야할 API는 3개이고
멤버 조회, 멤버 전체조회, 멤버 삭제 이렇게 된다.

1). GET

@Test
public void getMemberTest() throws Exception {
    MemberDto.response response = StubData.MockMember.getSingleResponseBody();
    long memberid = response.getMemberId();

    // findMember(); Mock 처리하기 , 반환 필요x (새로운 객체)
    given(memberService.findMember(Mockito.anyLong())).willReturn(new Member());

    // memberToMemberResponse(); Mock 처리하기 , 반수필수 타입 = MemberDto.response
    given(mapper.memberToMemberResponse(Mockito.any())).willReturn(response);

    ResultActions actions = mockMvc.perform(
            get("/v11/members/{member-id}", memberid)
                    .accept(MediaType.APPLICATION_JSON)
    );

    actions
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.data.memberId").value("1"))
            .andExpect(jsonPath("$.data.email").value("dhfif718@gmail.com"))
            .andExpect(jsonPath("$.data.name").value("이재혁"))
            .andExpect(jsonPath("$.data.phone").value("010-1234-5678"))
            .andDo(document("get-findMember",
                    getRequestPreProcessor(),
                    getResponsePreProcessor(),
                    pathParameters(
                            parameterWithName("member-id").description("회원 식별자")
                    ),
                    responseFields(
                            List.of(
                                    fieldWithPath("data").type(JsonFieldType.OBJECT).description("결과 데이터"),
                                    fieldWithPath("data.memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
                                    fieldWithPath("data.email").type(JsonFieldType.STRING).description("이메일"),
                                    fieldWithPath("data.name").type(JsonFieldType.STRING).description("이름"),
                                    fieldWithPath("data.phone").type(JsonFieldType.STRING).description("전화 번호"),
                                    fieldWithPath("data.memberStatus").type(JsonFieldType.STRING).description("회원 상태: 활동중/휴면상태/탈퇴상태"),
                                    fieldWithPath("data.stamp").type(JsonFieldType.NUMBER).description("스탬프 갯수")
                            )
                    )
            ));
}

크게 달라진점은 없다.
andDo를 통해 검증과 동시에 API 자동화를 위한
여러가지 파라미터들을 넘겨주고 있다.

GET 요청이기때문에 requestFields();에 대한
작성은 따로하지 않았다.

{
  "data" : {
    "memberId" : 1,
    "email" : "dhfif718@gmail.com",
    "name" : "이재혁",
    "phone" : "010-1234-5678",
    "memberStatus" : "활동중",
    "stamp" : 0
  }
}

최종 적으로 responseFields의 Json 형태는 위와 같이 이루어진다.
현재는 data라는 Object안에 한종류의 데이터가 들어가 있다.


만약 전체조회를 위해
data안에 배열로 데이터가 존재한다면 어떻게
표시를 해주어야할까?

responseFields(
        List.of(
                fieldWithPath("data.[].memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
                fieldWithPath("data.[].email").type(JsonFieldType.STRING).description("이메일"),
                fieldWithPath("data.[].name").type(JsonFieldType.STRING).description("이름"),
                fieldWithPath("data.[].phone").type(JsonFieldType.STRING).description("전화 번호"),
                fieldWithPath("data.[].memberStatus").type(JsonFieldType.STRING).description("회원 상태: 활동중/휴면상태/탈퇴상태"),
                fieldWithPath("data.[].stamp").type(JsonFieldType.NUMBER).description("스탬프 갯수"),
        )
)

responseFields의 위와 같이 data.[].을 이용해
배열에 해당하는 내용을 표시해줄 수 있다.

또한

responseFields(
        List.of(
                fieldWithPath("data").type(JsonFieldType.ARRAY).description("결과 데이터").optional(),
                fieldWithPath("data[].memberId").type(JsonFieldType.NUMBER).description("회원 식별자"),
                fieldWithPath("data[].email").type(JsonFieldType.STRING).description("이메일"),
                fieldWithPath("data[].name").type(JsonFieldType.STRING).description("이름"),
                fieldWithPath("data[].phone").type(JsonFieldType.STRING).description("전화 번호"),
                fieldWithPath("data[].memberStatus").type(JsonFieldType.STRING).description("회원 상태: 활동중/휴면상태/탈퇴상태"),
                fieldWithPath("data[].stamp").type(JsonFieldType.NUMBER).description("스탬프 갯수"),
        )
)

위와 같이도 표기할 수 있다.

여기서 한가지 참고해야할 점은 JsonFieldType.OBJECT = data : {}
JsonFieldType.ARRAY = data : []
과 같이 표기할 때 사용한다는 점이다.

하나의 멤버만 조회했을때 OBJECT를 사용한 이유와
여러 멤버를 조회했을때 ARRAY를 사용한 이유이다.

{
  "data" : [ {
    "memberId" : 1,
    "email" : "dhfif718@gmail.com",
    "name" : "이재혁",
    "phone" : "010-1234-5678",
    "memberStatus" : "활동중",
    "stamp" : 0
  }, {
    "memberId" : 2,
    "email" : "tmdghwlq@naver.com",
    "name" : "염승호",
    "phone" : "010-2000-4000",
    "memberStatus" : "활동중",
    "stamp" : 0
  }, {
    "memberId" : 3,
    "email" : "dragonwon@nate.com",
    "name" : "정용원",
    "phone" : "010-7777-4587",
    "memberStatus" : "활동중",
    "stamp" : 0
  } ]
}

실제로 build하여 생성된 response-body를 확인해보면
우리가 최종적으로 반환한 데이터에 따른 배열이
위와 같이 잘표시되는 모습을 확인해 볼 수 있다.

이렇게 복잡한 구조의 Json형태일지라도
배열에 대한 위치만 잘 지정해주면 @Test 컴파일시 정상적으로
실행되어 스니핏이 잘 만들어 지는 것을 확인할 수 있다.


2). DELETE

@Test
public void deleteMemberTest() throws Exception {
    long memberId = 1L;

    // deleteMember(); Mock 처리하기, 반환타입 = void
    doNothing().when(memberService).deleteMember(memberId);

    ResultActions actions = mockMvc.perform(
            delete("/v11/members/{member-id}", memberId)
                    .accept(MediaType.APPLICATION_JSON)
    );

    actions
            .andExpect(status().isNoContent())
            .andDo(document(
                    "delete-member",
                    getRequestPreProcessor(),
                    getResponsePreProcessor(),
                    pathParameters(
                            parameterWithName("member-id").description("회원 식별자")
                    )
            ));
}

Delete는 사실 더욱더 간단하다.
requestFields와 responseFields의 따로 만들어줄
필요가 없기 때문에 parameter에 대한 API 내용만 적어주면된다..

이렇게 오늘은 GET,DELETE에 대한
API 문서 자동화를 진행했다.


오늘 마주한 ERROR

1). 문제 확인
GET, DELETE에 대한 테스트케이스는 통과한 상태로
gradle build하는 과정 중 문제가 발생했다.

첫번째는 아래와 같은 오류 코드가 발생

There were failing tests. See the report at: file:///Users/ljh/Desktop/myprogram/CodeStates/be-homework-api-documentation/build/reports/tests/test/index.html

두번째는 resources의 index.html 파일이 생기지 않았다.

image

첫번째 문제로 인해 파일 생성이 되지 않은 듯 했다.


2). 문제 해결
첫번째 오류 코드를 천천히 보니 테스트를 실패했으니
해당경로에 들어가서 보라는 것 같았다.

image

해당 경로에 들어가서 index.html 파일을 열어보았다.

image

열어보았더니 Test Summary라는
테스트 결과에 대한 요약을 나타내주는 화면이 있었고

MemberControllerTest4의 getMembersTest() 메서드가
테스트를 통과하지 못했구나라고 알아차렸다.

해당 문제는 내가 GET, DELETE를 추가하면서
기존에 둔 테스트코드에 영향이 갔었고
오류를 수정하고 다시 gradle build를 하니

image

index.hmtl 파일이 생성된 모습을 확인할 수 있다 !!


오늘은 API 자동화를 위한
Spring Rest Docs를 연습해보는 시간을 가졌다.
이런식으로 테스트코드와 연결해
API 문서를 만들 수 있다는게 너무 좋은 기술인 것 같다는 생각이든다.

사실 이전까지만해도 수동으로 사람이 다적어서 관리하는 줄 알았는데
이렇게 좋은 방법으로 관리를 할 수 있다는 점에서
좋은 기술을 배운 것 같아 뿌듯한 느낌이 들었다.

오늘 공부도 여기서 끝 !!


오늘의 커피량: ☕️ ☕️ ☕️
오늘의 점심: 삼겹살, 라면, 밥