Projects/Personal project - 게시판 만들기

MockMVC - Multipart form data요청 테스트

마손리 2023. 6. 10. 22:46

아무생각 없이 테스트 코드를 작성하던 도중 Multipart form data 타입의 요청을 테스트 할 차례가 다가 왔다.

 

일단 Multipart form data 타입의 요청을 테스트하고 해당 테스트를 기반으로 RestDocs를 이용하여 API문서를 만들기 위해서는 두가지 기능을 구현할 방법을 알아야 했다.

  1. MockMvc에서 Multipart form data 타입의 요청을 보내는 방법
  2. 해당 요청을 테스트한 뒤 RestDocs를 이용하여 API 문서로 만드는 방법

 

해당 기능을 수행하기에 앞서 request body의 형식이다.

"requestBody"라는 key로 json형식의 데이터와 "photoImgs"의 key로 이미지파일이 들어가야 한다.

 

 

 

1. MockMvc에서 Multipart form data 타입의 요청을 보내는 방법

 

@WebMvcTest(value = PostsController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class PostsControllerRestDocsTest implements PostsControllerTestHelper {
    ...
    ...
    @Test
    @WithMockUser
    public void postMemberTest() throws Exception {
    	// key, requestBody에 들어갈 json 형식의 데이터
        String content = "{\t\"title\":\"the Title\",\t\"postStatus\": \"public post\",\t\"content\" : \"the content\"}";
		
        // key, 요청 컨텐트 타입, 데이터를 이용하여 MockMultipartFile을 인스턴스화
        MockMultipartFile metadata = new MockMultipartFile("requestBody", "",
                "application/json", content.getBytes());
                
        // 마찬가지로 IMG파일을 대신할 가짜파일을 만들어 준다.
        MockMultipartFile image = new MockMultipartFile("photoImgs", "image.png", "image/png",
        "<<Img data>>".getBytes());

		// 만들어진 가짜 파일들로 mockMvc를 이용하여 테스트를 위한 가짜 요청을 보냄
        ResultActions actions = mockMvc.perform(
                RestDocumentationRequestBuilders.multipart("/v1/posts")
                        .file(image)
                        .file(metadata)
                        .accept(MediaType.APPLICATION_JSON)
                	.with(SecurityMockMvcRequestPostProcessors.csrf()));

    }
}

 

해당 테스트에서는 json형식의 데이터와 이미지파일 모두 요청으로 보내야 하므로 MockMultipartFile을 이용하여 두개의 가짜 파일을 만들어 준다. 

 

이때 사용될 key인 "requestBody""photoImgs"는 포스트맨에서 요청을 보낼때와 동일하게 해당 컨트롤러에 명시해 준대로 적어주어야 한다.

 

 

 

이후 파일의 오리지널 이름, 파일의 타입, 파일의 내용을 나머지 생성자 매개변수로 넣어주는데 이때 파일의 내용은 getBytes() 메서드를 이용하여 인코딩해준다.

 

 

요청을 보내는 작업에서 기존에는 RestDocumentationRequestBuilders 클래스의 post()나 patch()와 같은 메서드들을 사용했다면 이번에는 multipartForm() 메서드를 사용하여 위에서 생성한 가짜 파일들을 첨부한 뒤 요청을 보내게 된다.

 

 

2. RestDocs를 이용하여 API 문서로 만드는 방법

 

위와 같이 가짜 요청을 보내주었으면 이제 해당 요청으로 테스트를 진행하고 RestDocs을 이용하여 API문서로 만들어야 하는데 기존에는 PayloadDocumentation의 requestFields() 메서드를 사용해 주었지만 이번에는 requestPartFields()requestPartBody()를 사용해준다.

 

@WebMvcTest(value = PostsController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class PostsControllerRestDocsTest implements PostsControllerTestHelper {
    ...
    ...
    @Test
    @WithMockUser
    public void postMemberTest() throws Exception {
        ResultActions actions = 
        	...
                ...

        actions
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", is(startsWith("/v1/posts"))))
                .andDo(document("post-posts",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        requestPartFields("requestBody", // API 문서에 표시될 requestBody의 정보
                                List.of(
                                        fieldWithPath("title").type(JsonFieldType.STRING).description("게시글 제목"),
                                        fieldWithPath("postStatus").type(JsonFieldType.STRING).description("게시글 상태"),
                                        fieldWithPath("content").type(JsonFieldType.STRING).description("게시글 내용")
                                )),
                        requestPartBody("photoImgs"), // photoImgs의 정보
                        responseHeaders(
                                headerWithName(HttpHeaders.LOCATION).description("Location header. 등록된 리소스의 URI")
                        )
                ));
    }
}

 

기존에 사용하던 requestFields()와 사용법이 흡사하다. 다만 해당 파트의 key인 "requestBody""photoImgs"를 각각 넣어준다. 

 

requestPartBody() 메서드로 해당 키를 넣어준다면 위에서 작성한 가짜 파일, 즉 MockMultipartFile의 내용으로 명시해준 <<IMG data>>로 API 문서화가 진행된다.

 

 

제작된 API 문서

requestBody

 

photoImgs

 

전체 코드

@WebMvcTest(value = PostsController.class)
@MockBean(JpaMetamodelMappingContext.class)
@AutoConfigureRestDocs
public class PostsControllerRestDocsTest implements PostsControllerTestHelper {
    @Autowired
    private MockMvc mockMvc;
    @MockBean
    private PostsService postsService;
    @MockBean
    private PostsMapper mapper;

    @Test
    @WithMockUser
    public void postMemberTest() throws Exception {
        String content = "{\t\"title\":\"the Title\",\t\"postStatus\": \"public post\",\t\"content\" : \"the content\"}";

        Posts mockResultPost = new Posts();
        mockResultPost.setPostId(1L);

        given(mapper.postDtoToPosts(Mockito.any(PostsDto.Post.class))).willReturn(new Posts());

        given(postsService.createPost(Mockito.any(Posts.class),Mockito.anyList())).willReturn(mockResultPost);

        MockMultipartFile image = new MockMultipartFile("photoImgs", "image.png", "image/png",
                "<<Img data>>".getBytes());
        MockMultipartFile metadata = new MockMultipartFile("requestBody", "",
                "application/json", content.getBytes());

        ResultActions actions = mockMvc.perform(
                RestDocumentationRequestBuilders.multipart("/v1/posts")
                        .file(image)
                        .file(metadata)
                        .accept(MediaType.APPLICATION_JSON)
                .with(SecurityMockMvcRequestPostProcessors.csrf()));

        actions
                .andExpect(status().isCreated())
                .andExpect(header().string("Location", is(startsWith("/v1/posts"))))
                .andDo(document("post-posts",
                        preprocessRequest(prettyPrint()),
                        preprocessResponse(prettyPrint()),
                        requestPartFields("requestBody",
                                List.of(
                                        fieldWithPath("title").type(JsonFieldType.STRING).description("게시글 제목"),
                                        fieldWithPath("postStatus").type(JsonFieldType.STRING).description("게시글 상태"),
                                        fieldWithPath("content").type(JsonFieldType.STRING).description("게시글 내용")
                                )),
                        requestPartBody("photoImgs"),
                        responseHeaders(
                                headerWithName(HttpHeaders.LOCATION).description("Location header. 등록된 리소스의 URI")
                        )
                ));
    }
}

 

 

 

 

참고 자료 - 

https://godekdls.github.io/Spring%20REST%20Docs/documentingyourapi/#362-documenting-a-request-parts-fields

 

RestDocs의 optional과 constraint 정보 문서화 -

https://mason-lee.tistory.com/120