ㅎㅎ

코드 굽는 제빵사·2021년 1월 21일
0

문제 : 무언가 잘못 되었다.

지금까지 인프런 김영한님의 강의를 듣고 스스로 스프링을 학습하기 위해 코드를 작성했습니다.
물론 미숙하다보니 코드를 작성 하는 시간보다 찾아보는 시간이 길었습니다.
그리고 코드부터 작성하고 테스트코드를 만들어야 한다고 들었고 해보려고 했습니다.
그런데 테스트 코드를 통과하기 위해 작성해서 힘들기도 했고 엔티티가 변경되면 많은 테스트를
변경 해야해서 하기 싫어지기 시작했습니다. 코드를 작성하는 시간보다 테스트를 작성하는 시간이
늘어났고 그래서 이게 맞는걸까라는 의구심이 들기 시작 했습니다.
그래서 이 문제를 해결하기 위해 송파도서관에서 책을 대출하였습니다.

해결 : TDD로 처음부터 다시 해보자!

켄트 백님의 테스트 주도 개발, 이대엽님이 옮기신 테스트 주도 개발로 배우는 객체 지향의 설계와 실천이란
책을 읽고 해보기로 했습니다.

책을 읽고 가장 먼저 느낀 것은 테스트 통과만을 위한 테스트 작성이 아니라 어떠한 픽쳐를 가정하고
테스트를 작성하면서 필요한 클레스를 만들어 나가는 것이었습니다.
이것을 적용 하려면 큰 덩어리부터 시작하면 엄두도 나지 않을 것이기에 Store 가게라는 도메인의 CRUD를
Controller부분에서 인수부터 리턴값을 가정하고 Top-down 방식으로 생각하면서 구현하기로 했습니다.

Feature: 지하철역 근처의 구인중인 가게 찾기

  • 가게 CRUD

Scenario: 가게정보를 등록 하려고 한다.
1. Given 가게의 정보를 입력 하고.
2. When 등록 하려고 하면
3. Then 등록 후 가게의 아이디 값을 반환 한다.

왜냐하면 아직 구현하지 않은 서비스 계층을 사용하기 때문이다. Mock객체를 사용해서 수고로움 더 있지만
협업이라고 가정 한다면 파라미터로 받을 인수와 반환 형태를 먼저 정해집니다.
그렇게 되면 상세 사항이 나올 때까지 프론트엔드 개발이 기다리지 않고 시작 할 수 있을 것 같습니다.

@WebMvcTest(StoreCmdController.class)
@AutoConfigureWebMvc
@ExtendWith(SpringExtension.class)
public class StoreCmdControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Autowired
    ObjectMapper objectMapper;

    @MockBean
    StoreCmdServiceImpl storeService;


    @Test
    public void registerStore() throws Exception {
        //given
        StoreCmdRequest storeCmdRequest = getStoreCmdRequest();
        //when, then
        given(storeService.registerStore(any(StoreCreateDto.class))).willReturn(1L);
        mockMvc.perform(post("/api/stores")
                .contentType(MediaType.APPLICATION_JSON)
                .accept(MediaType.APPLICATION_JSON)
                .content(asJsonString(storeCmdRequest)))
                .andDo(print())
                .andExpect(jsonPath("id").value(1L));
    }
}

테스트 케이스에서 한줄 또는 한 단어를 작성하면서 빨간불이 들어오면 그것을 인텔리제이의 단축키를 조합하여
만들어 나가면서 레드 -> 그린으로 변경하면서 코드를 만들어 나갔습니다.
처음엔 에러에서 초록불로 변경 하는 것에 초점을 두어 해결했고 이후에는 나 이외에 다른 사람들이 알아 들을 수 있게 함수로 변경하면서 진행 했습니다.

@PostMapping("/api/stores")
    public StoreCmdResponse registerStore(@RequestBody @Valid StoreCmdRequest request) {
        StoreCreateDto storeCreateDto = new StoreCreateDto(request.getName(),
                request.getCity(),
                request.getStreet(),
                request.getZipcode());
        return new StoreCmdResponse(storeService.registerStore(storeCreateDto));
    }

Scenario: 가게정보를 업데이트 하려고 한다.
1. Given 기존 가게 정보가 주어지고
2. When 정보를 수정하려고 하면
3. Then 수정 후 수정 된 정보를 반환 한다.

    @Test
    public void updateStore() throws Exception {
        //given
        UpdateStoreRequest updateRequest = new UpdateStoreRequest("seoul", "songpa-dong", "zipcode");
        StoreUpdateDto resultDto = new StoreUpdateDto("samsung", "seoul", "songpa-dong", "zipcode");
        given(storeService.updateStore(any(Long.class), any(StoreUpdateDto.class))).willReturn(resultDto);
        //when then
        mockMvc.perform(put("/api/stores/{id}", 1)
        .contentType(MediaType.APPLICATION_JSON)
        .accept(MediaType.APPLICATION_JSON)
        .content(asJsonString(updateRequest)))
                .andDo(print())
                .andExpect(jsonPath("name").value("samsung"))
                .andExpect(jsonPath("city").value("seoul"))
                .andExpect(jsonPath("street").value("songpa-dong"))
                .andExpect(jsonPath("zipcode").value("zipcode"));
    }

컨트롤러는 service에게 책임을 위임하고 서비스의 결과를 전달 받아 그것을 Response로
반환 하도록 만들었습니다.

@PutMapping("/api/stores/{id}")
    public UpdateStoreResponse updateStore(@PathVariable("id") Long id, @RequestBody @Valid UpdateStoreRequest request) {
        StoreUpdateDto updateStore = storeService.updateStore(id, createUpdateDto(request));
        return createUpdateStoreResponse(updateStore);
    }

Scenario: 가게정보를 조회 하려고 한다.
1. Given 가게의 아이디가 주어진다.
2. When 아이디로 가게를 조회하면
3. Then 가게의 정보를 반환 한다.

 @Test
    public void findById() throws Exception {
        //given
        StoreQueryDto resultDto = new StoreQueryDto("starbucks", "city", "street", "zipcode");
        given(storeQueryService.findById(any(Long.class))).willReturn(resultDto);
        //when //then
        mockMvc.perform(get("/api/stores/{id}", 1))
                .andExpect(jsonPath("name").value("starbucks"));
        verify(storeQueryService, times(1)).findById(any(Long.class));
    }

ID를 기반으로 정보를 조회하는 것이기 때문에 별도의 DTO 변경없이 아이디를 바로 넘겨주면
해결 할 수 있기에 위임 하는것으로 만들었습니다.

@GetMapping("/api/stores/{id}")
    public StoreQueryDto findById(@PathVariable("id") Long storeId) {
        return storeQueryService.findById(storeId);
    }

Scenario: 가게정보를 삭제 하려고 한다.
1. Given 가게의 아이디가 주어진다.
2. When 아이디로 가게로 삭제를 하려고 하면
3. Then 가게의 정보를 삭제되고 반환 값은 없다.

void형태를 테스트 하는 것이 가장 눈에 보이지 않아서 난감했습니다.
이와 같은 경우 삭제의 역활을 위임한 deleteStore 메소드의 호출 횟수를 확인하는 것으로 테스트 했습니다.

@Test
    public void deleteStore() throws Exception {
        //given
        Store store = new Store(1L, "starbucks", null);
        //when //then
        doNothing().when(storeService).deleteStore(store.getId());
        mockMvc.perform(delete("/api/stores/{id}", store.getId()))
                .andDo(print())
                .andExpect(status().isOk());
        verify(storeService, times(1)).deleteStore(any(Long.class));
    }

컨트롤러는 Http 요청에 대해 처리하고 서비스로직은 deleteStore에서 처리 하도록 합니다.

@DeleteMapping("/api/stores/{id}")
    public void deleteStore(@PathVariable("id") Long storeId) {
        storeService.deleteStore(storeId);
    }

0개의 댓글