ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot][토이 프로젝트] 1. KakaoMap API 백엔드처리 및 테스트 코드 작성
    Spring/Study 2023. 6. 29. 18:21

    토이 프로젝트 설명

    목적

    본 프로젝트의 목적은
    첫 번째로 프론트의 카카오 맵 API 활용을 연습하려는 목적 백엔드의 지도 API 활용 방법 공부이다.
    두 번째로 Controller의 테스트 코드를 작성하기위해 WebMvcTest 활용 방법을 연습했고
    세 번째로 기상청 공공 API를 활용한 외부 데이터 수집 방법에 대한 학습을 목적으로 WebClient를 활용했다.

    이번 글은 첫 번째와 두 번째의 내용을 정리했고, 추가적인 글을 통해 세 번째의 내용을 정리할 예정이다.

    사용한 기술 및 스택

    언어 : JAVA

    프레임워크 : Spring Boot, JPA, WebFlux

    데이터베이스 : H2, MariaDB

    DevOps : AWS, Docker

    라이브러리 : Lombok, H2, MariaDB JDBC, QueryDSL, Netty, JSON-Simple

    프로젝트 기간

    23.06.18 ~ 23.06.24

    이후 테스트 코드 연습 및 코드 리팩토링 지속중...

    KakaoMap API 학습 기록

    KakaoMap API 활용

    백엔드 관점에서의 지도 API

    백엔드 관점에서의 지도 API 사용 방법은 간단했었다. 특정 건물의 위도, 경도를 저장하고 그 위치의 건물 이름 및 건물에 대한 상세 정보를 저장하면 끝이였다.

    간단하게 Entity 클래스를 작성해봤다.

    package com.list.kakaoMap.entity;
    
    import jakarta.persistence.*;
    import lombok.*;
    
    @Entity
    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    public class Store {
    
        @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
        @Column(name = "store_id")
        private Long id;
    
        @Column
        private String name;
        @Column
        private String detail;
        @Column
        private double posX;
        @Column
        private double posY;
    
    }
    

    Store 라는 엔티티에 이름, 상세정보, X, Y 값을 매핑 시켜뒀다.

    간단한 이름을 통한 조회

    간단하게 CRUD 구현만 진행하였으나, 건물 검색기능 정도는 구현해서 사용해보는 방법이 좋을 것 같아서

    QueryDsl을 사용하여 입력받은 이름이 포함된 건물 조회 기능을 구현했다.

    public List<Store> findStore(String name) {
            QStore qStore = new QStore("store");
            return jpaQueryFactory
                    .selectFrom(qStore)
                    .where(qStore.name.contains(name))
                    .fetch();
    }

    파라미터로 name을 받으면 name 값을 지닌 Q타입 Store를 조회하도록 작성하였다.

    WebMvcTest

    Controller를 테스트 하기 위해 WebMvcTest를 사용해봤다.

    다음은 StoreController 전체 코드 내용이다.

    StoreController 전체 코드

    package com.list.kakaoMap.controller;
    
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.list.kakaoMap.entity.Store;
    import com.list.kakaoMap.service.StoreService;
    import jakarta.transaction.Transactional;
    import org.junit.jupiter.api.Disabled;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.api.extension.ExtendWith;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.boot.test.mock.mockito.MockBean;
    import org.springframework.http.MediaType;
    import org.springframework.test.context.junit.jupiter.SpringExtension;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.ResultActions;
    import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
    import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
    import org.springframework.util.LinkedMultiValueMap;
    import org.springframework.util.MultiValueMap;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import static com.list.kakaoMap.dto.StoreDto.*;
    import static org.mockito.ArgumentMatchers.any;
    import static org.mockito.BDDMockito.given;
    import static org.mockito.Mockito.when;
    import static org.springframework.http.RequestEntity.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
    
    @WebMvcTest(StoreController.class)
    class StoreControllerTest {
    
        @Autowired
        MockMvc mockMvc;
    
        @Autowired
        ObjectMapper objectMapper;
    
        @MockBean
        StoreService storeService;
    
        @Test
        @DisplayName("가게 등록 테스트")
        public void addTest() throws Exception {
            StoreRequest storeRequest = new StoreRequest("testId", "testDetail", 11.01, 27.09);
            when(storeService.addStore(any())).thenReturn(new Store(1L, "testId", "testDetail", 11.01, 27.09));
    
            ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post("/store")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(storeRequest)));
    
            resultActions
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andExpect(MockMvcResultMatchers.jsonPath("$.success").value(true))
                    .andDo(print());
        }
    
        @Test
        @DisplayName("가게 조회 테스트")
        public void findTest() throws Exception {
            //given
            Store storeResponse1 = new Store(1L, "test_7e6b067e0783", "test_c5d923cc5984", 97.45, 72.58);
            Store storeResponse2 = new Store(2L, "test_1123klnsas", "test_29sne215231", 23.56, 28.65);
            Store storeResponse3 = new Store(3L, "test_24901hsksa", "test_32kjba9e1", 55.27, 81.21);
            List<Store> storeResponseList = new ArrayList<>();
            storeResponseList.add(storeResponse1);
            storeResponseList.add(storeResponse2);
            storeResponseList.add(storeResponse3);
    
            when(storeService.findStore(any())).thenReturn(storeResponseList);
    
            MultiValueMap<String, String> param = new LinkedMultiValueMap<>();
            param.add("name", "test");
    
            //when
            ResultActions perform = mockMvc.perform(MockMvcRequestBuilders.get("/store")
                    .param("name","test"));
            //then
            perform.andExpect(MockMvcResultMatchers.status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.result").exists())
                    .andDo(print());
        }
    
        @Test
        @DisplayName("가게 정보 수정 테스트")
        public void editTest() throws Exception {
            //given
            StoreRequest storeRequest = new StoreRequest("test_12523as", "test_29sne215231", 23.56, 28.65);
            Store store = Store.builder()
                    .id(2L)
                    .name("test_12523as")
                    .detail("test_29sne215231")
                    .posX(23.56)
                    .posY(28.65)
                    .build();
    
            when(storeService.editStore(any(),any())).thenReturn(store);
            //when
            ResultActions perform = mockMvc.perform(MockMvcRequestBuilders.put("/store/" + 2)
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(storeRequest)));
            //then
            perform.andExpect(MockMvcResultMatchers.status().isOk())
                    .andExpect(jsonPath("$.success").value(true))
                    .andExpect(jsonPath("$.name").value("test_12523as"))
                    .andDo(print());
        }
    
        @Test
        @DisplayName("가게 삭제 테스트")
        public void deleteTest() throws Exception {
            //given
            given(storeService.deleteStore(1L))
                    .willReturn(true);
            //when
            ResultActions perform = mockMvc.perform(MockMvcRequestBuilders.delete("/store/" + 1));
            //then
            perform.andExpect(MockMvcResultMatchers.status().isOk())
                    .andExpect(jsonPath("$.success").value(true));
        }
    }

    애노테이션

    @WebMvcTest(StoreController.class)
    // StoreController 를 테스트 하기 위한 Test
    @MockBean
    // 가짜 빈을 등록하여 실제 로직처럼 행동하는 빈을 등록

    when .thenReturn

    가짜 빈을 등록했으므로 실제 서비스 계층에서 특정 메소드를 실행하면 리턴값을 직접 지정해주는 방법이다.

    when(storeService.addStore(any())).thenReturn(new Store(1L, "testId", "testDetail", 11.01, 27.09));

    가게 등록 테스트를 예시로 들면, "addStore()라는 메소드에 any() 어떤 파라미터값을 넣어도 new Store(...) 라는 객체가 반환 될 것이다." 라는 의미이다.

    ObjectMapper

    ObjectMapper는 Jackson 라이브러리의 클래스로 "JAVA의 객체 <--> JSON 값" 으로 서로 변경할 때 사용한다.

    StoreRequest storeRequest = new StoreRequest("testId", "testDetail", 11.01, 27.09);
    
    ...
    
    ResultActions resultActions = mockMvc.perform(MockMvcRequestBuilders.post("/store")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(storeRequest)));

    mockMvc로 POST로 바디 값을 전송해야 할 때, JSON 형식의 값을 전송하므로, 보내야할 storeRequest 값을 JSON 으로 변환 하는데 사용했다.

    objectMapper.writeValueAsString(storeRequest); // storeRequest 라는 JAVA 객체르 JSON 형태의 문자열로 변환 

    물론 본 테스트 코드에서는 작성하지 않았지만

    objectMapper.readValue(Json문자열, 변환할 DTO) // DTO에 맞춰서 JSON 문자열을 JAVA 객체로 변환

    과 같이 사용도 가능했다.

    댓글

Designed by black7375.