프로젝트 회고

Re:USE 프로젝트 中 테스트 코드 작성 회고

iksadnorth 2023. 11. 17. 20:25

👣 개요

해당 프로젝트는 개발 초기에 WireFrame이 확정되지 않았기 때문에 각 API가 전달해야 하는
응답 JSON 형식이 수시로 바뀌는 상황이었다. 때문에 Lazy Loading에 의존해서 API를 제작했고
Entity를 DTO로 변환하는 과정 중 N+1 문제가 발생했었다.

때문에 MVP 달성 이후 더이상 변경될 확률이 적은 API를 보다 최적화시키기 위해
Projection 기능과 적절한 Join 연산으로 1개의 쿼리로 필요한 모든 정보를 불러올 계획을 세웠다.

하지만 이전 프로젝트 경험 중 정상 작동되는 API를 함부로 수정했다가 오류를 일으킨 경우가 있었기에
코드 리펙토링 이전에 테스트 코드를 작성하고자 했다.

 

👣 테스트 계획

처음 테스트는 통합 테스트로 진행하려 했었다.
왜냐 하면, 단순히 리팩토링을 위한 코드 작성뿐이지 개발을 위한 테스트 코드 작성은 아니었기에
굳이 단위 테스트까지 쪼개서 코드를 작성할 필요는 없다고 생각했다.
정상 작동 여부만 확인할 뿐 버그를 유발하는 코드 위치까지 특정 짓기 위해
추가적인 코드는 시간과 에너지를 과하게 소비할 뿐이라고 여겼다.

하나의 API에서 확인하고 싶었던 항목은 다음과 같았다.

1. JWT를 헤더에 포함시켰을 때, 인가 기능이 제대로 작동하는지?
2. 정상 작동할 조건일 때, 200 상태코드를 띄우며, 예상된 계산 결과를 출력하는지?

오류를 내는 경우도 테스트 코드에 반영하고 싶었지만 워낙 API 갯수도 많았고
MVP 달성까지 시간도 부족했기 때문에 비정상 작동 테스트 코드는 작성하지 못했다.

API 자체를 이용해서 비즈니스 코드 작동 여부를 판단하려고 했다.
무슨 소리냐면 Service에 입력될 데이터를 애초에 Controller부터 전달하려고 했다.
정말 React 팀에서 사용하는 환경과 유사하게 만들어서 테스트 효용을 극대화하고 싶었기 때문이다.
뿐만 아니라 오로지 리펙토링 시, 문제 발생 여부만 확인하고 싶어서 테스트 코드에 너무 많은 시간을 투자하기 싫었던 것도 있었다.

@SpringBootTest
@AutoConfigureMockMvc
public class UserControllerIntegrationTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testGetUserById() throws Exception {
        // 사용자 ID가 1인 사용자를 가져오는 API를 호출하고 응답을 검증합니다.
        mockMvc.perform(MockMvcRequestBuilders.get("/users/1"))
               .andExpect(MockMvcResultMatchers.status().isOk())
               .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1))
               .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("John Doe"));
    }
}

 

하지만 위 경우, 실제로 사용하기 어려운 점이 있었다.
위에서 언급했듯이 WireFrame이 꽤나 가변적이었고 JSON의 Key값 형태가 자주 변경되었다.
예를 들어, "id" -> "item_id"와 같이 React 팀의 요청에 의해 변경되는 경우가 빈번했다.

이런 경우, 기존에 작성된 테스트 코드를 너무 자주 변경했어야 할 뿐만 아니라
테스트 코드 자체도 내가 예상한 값이 출력되는 Key값을 하나하나 모두 찾아서 매핑해야 했다.

 

👣 테스트 코드 작성 결과

때문에 Controller Layer는 특별히 비즈니스 코드와 분리해 다루기로 했다.
이렇게 함으로서 1개의 API당 최소 2개의 테스트 코드를 작성하게 되었고 형태는 다음과 같았다.

FollowAuthTest 중 일부
FollowModelTest 중 일부
classpath:data/testcase-follow.sql
classpath:truncate-testcases.sql

Service Layer와 Repository Layer는 의도적으로 분리하지 않았는데
그 이유는 Service에서 사용하는 Repository의 함수가 변경되었다고 해서
테스트 코드가 그에 따라 휘둘리며 Fail을 띄우지 않게 하기 위해 이와 같이 수행했다.

한 줄로 요약하면, 비즈니스 코드와 테스트 코드 사이의 지나치게 큰 의존성을 제거하고 싶었다는 것이다.

완벽하게 모든 경우에 대해 테스트를 진행하지는 못했지만
총 119개의 테스트 코드를 작성하는 경험을 할 수 있었다.