미니멀 프론트엔드 테스트

테스트 커버리지는 얼마를 달성해야 좋을까요? 모든 코드는 테스트 대상이 돼야할까요? 하지만 테스트 작성 또한 비용이 들고 유지보수 대상이 됩니다. 이는 생산성 저하를 초래하여 프로젝트에 지장을 줄 수 있습니다.

💎 실패할 가치가 있는 테스트를 작성한다

“작은 함수로 테스트를 시작하는 건 좋은 접근이었습니다. 하지만 그게 private한 함수라면 효용성이 낮죠.“

회사에서 처음 작성한 테스트를 본 팀 리드(Ted)의 피드백이었습니다. 분기 목표로 맡고있던 프로젝트의 ‘테스트 커버리지 10% 달성’(당시 0%)‘을 잡았고, 일단 가장 시작하기 쉬워 보이는 부분을 찾아 테스트를 작성했습니다. 하지만 결과적으로 이는 근시안적인 접근이었습니다.

테스트를 추가했던 부분은 다음과 같이 생겼습니다.

function MyComponent() {
  /* 초기화 */

  const someState = (() => {
    if (/* condition1 */) return 'a';
    if (/* condition2 */) return 'b';
    if (/* condition3 */) return 'c';
  })()

  return <>{/* someState를 사용하는 JSX */}</>
}

someState를 만들어내는 부분이 즉시실행 함수(IIFE)로 컴포넌트 코드 내에 선언돼 있었고, 여러 개의 조건문으로 인해 가독성이 떨어지는 상황이었습니다. 이 복잡한 IIFE를 밖으로 빼내고 테스트를 만드는 게 가장 쉬워 보였습니다. 작게라도 케이스를 만들면, 동료들도 제가 작성한 가이드를 보면서 “이렇게 만들었구나”하고 쉽게 받아들일 수 있도록 하자는 생각이었습니다. 테스트 작성 후에는 조금이라도 테스트 커버리지가 올랐다는 생각에 뿌듯함도 살짝 느꼈습니다.

하지만 Ted와 테스트를 검토한 결과, 추출된 함수는 완벽히 MyComponent의 private 함수였습니다 private 함수는 테스트해봤자 큰 효용이 없다는 말에 수긍했습니다.

‘private’의 의미를 단순히 private 키워드 사용여부로 한정 짓지 않고 있습니다. 외부로 노출되지 않고, 재사용되지 않으면 ‘private 하다’라는 의미로 사용하고 있습니다.

요구사항이 바뀌어 구현이 변경돼야 한다면, 개발자는 직관적으로 위 함수만 변경해야 함을 알 수 있습니다. 당연히 작성한 테스트도 바뀌어야합니다. 여기서 private 함수 테스트가 큰 가치를 제공하지 못했다는 눈치 챌 수 있습니다. 오히려 동어반복적인 작업을 하는 부담만 줄 뿐입니다.

만약 함수가 public 했다면? 다른 곳에서도 사용됐다면? 변경으로 인해 다른 곳에 작성된 테스트가 실패했을 것이고 개발자는 미리 버그를 잡을 수 있게 됩니다. 테스트는 실패할 확률이 높을 때 가치가 올라갑니다.

여러 곳에서 여러 방법으로 사용되는 코드일수록 테스트할 가치가 높으며, 깨졌을 때 유용한 정보를 얻을 수 있습니다. 위 코드에서는 차라리 하나의 화면을 책임지면서 public하기도 한 MyComponent 전체를 테스트하는게 더 가치가 높다고 볼 수 있습니다.

TDD를 하고 있다면 얼마든지 private 함수를 테스트하게 될 수 있습니다. 물론 그 브랜치가 머지되기 전에 private 함수 테스트를 걷어내야겠지만요.

by Ted

😔 테스트만을 위한 테스트는 피한다

앞선 교훈을 발판삼아 이번엔 여러 곳에서 공통으로 사용하는 코드를 살펴봤습니다. 프로젝트 구조를 살짝 설명드리자면, 여러 페이지에 걸쳐 유사한 비즈니스 로직을 활용하는 코드를 분리해서 domains 라는 폴더 밑에서 별도로 관리하고 있습니다. 태생부터가 여러 곳에서 사용하기 위한 코드이니만큼, domains 하위의 코드들은 분명 테스트 대상 1순위라고 볼 수 있습니다.

function useComplexHook() {
  const { data1 } = useDomainRelated1() // API 호출
  const { data2 } = useDomainRelated2() // API 호출

  /* 복잡한 data1 & data2 가공 및 처리 */

  return {
    // data1 & data2 기반 복잡한 객체
  }
}

위 custom hook은

  • API 호출
  • 데이터 가공 및 처리
  • 여러 key/value를 갖는 객체 리턴

크게 3가지 부분으로 이루어져 있습니다. 나름 테스트에 익숙해진 지금 보자면 API 호출부를 mocking하고, 리턴값을 assertion하여 테스트를 작성하면 되겠다는 생각이 바로 떠오릅니다.

하지만 아직 react hook 테스트가 익숙치 않았던 당시엔 ‘테스트하기 쉽게 데이터 가공/처리 부를 별도 함수로 빼내서 테스트하는 건 어떨까’ 생각 했습니다만… 이는 첫번째 사례에서 저질렀던 실수의 반복이었습니다. useComplexHook의 private한 함수를 만들어 테스트하는 꼴입니다.

Ted는 추가로 생각할 지점을 일러주었습니다. 유지보수하기 더 쉽게 코드 구조를 바꾸고, 그럼으로써 테스트하기 쉽게 되는 건 좋습니다. 그러나 테스트하기 쉬워진다는 이유만으로 코드 구조를 바꾸는 것은 논리의 선후가 뒤바뀐 발상이라는 것입니다.

✍ 테스트 작성 전 고려사항

TDD를 하고있는 상황이 아니라면, 테스트를 작성시 “Why”를 먼저 생각해 봐야합니다. 단순히 커버리지에 집중했다간 의미없이 시간낭비만 하게 되는 꼴이니까요. 테스트 작성시 다음 사항들을 고려해봅시다.

  • 테스트 대상이 여러 곳에서 사용되는가?
  • 자명하지 않은 이유로 실패할 수 있는 테스트인가?
  • 테스트를 더 용이하게 만들기 위해 기존 코드구조를 바꿔야 한다면, 테스트 이외의 측면에도 이점이 있는가?

부족한 경험과 지식으로 테스트를 작성하다보면, 생각 이상으로 시간을 잡아먹는 부분이 많이 발생합니다.(삽질을 많이 하게 됩니다…) 그리고 때론 내가 잘 하고 있는건지, 허튼짓을 하는건 아닌지 의문이 들기도 합니다. 요구사항은 바뀌기 마련이고 이에따라 코드도 변경해야 하는데, 작성해둔 테스트가 짐짝처럼 느껴질 수도 있습니다. 이런 경험이 반복되면 테스트 작성 의지를 잃어버릴 수도 있습니다.

하지만 위 3가지 질문의 대답이 모두 Yes라면, 결코 시간낭비를 하고 있는건 아니니 용기 내어 마음껏 테스트를 작성해보시길 바랍니다. 분명 그 노력은 헛되지 않고, 이후 여러분이 즐겁고 마음 편히 프로젝트를 진행하도록 도와줄 것입니다. 🙏


Written by@Phil
FE 개발자

GitHubLinkedInYoutube