프론트엔드 테스트 코드를 도입하자 - 1. 어떤 것을 해야 하나
회사에서 한두 달 전에 새로 시작한 프론트엔드 프로젝트에 테스트를 도입하기로 했다.
고백하자면, 테스트를 적용한 경험은 적다. 기껏해야 간단한 유틸 함수들에 대한 유닛 테스트가 전부다. 통합 테스트는 고사하고 뷰 테스트, 컴포넌트 테스트 등도 해 본 적 없다.
하지만 언제까지 이렇게 테스트를 외면할 수는 없다. 애자일한 프로그래머를 지향하는 자, 테스트를 멀리할 수 없다. 아직 프로젝트가 초기 단계인 지금이 기회라고 생각하고 하나하나 차근차근 적용해 보려고 한다.
그럼 일단 프론트엔드 소프트웨어 테스트에는 어떤 것들이 있는지, 어떻게 도입하면 좋을지부터 간단히 정리해보겠다.
(진행하고 있는 프로젝트가 React Native 프로젝트이기 때문에 React Native 관점에서 작성한다. 하지만 사용하는 세부 라이브러리들만 다를 뿐, 개념적으로는 React 및 기 타 다른 프론트엔드 프로젝트에도 적용 가능하다.)
목차
- 테스트해야 할 것들
- 결론
- 참고 자료
테스트해야 할 것들
테스트를 고려할 수 있는 것은 크게 다섯 가지가 있다.
- 선행할 것들
- 단위 테스트
- React 테스트
- API 콜 테스트
- 통합 테스트
- 기타 테스트
각각 어떤 것이 중요한지, 어떻게 적용하면 좋을지 간단히 살펴보자.
0. 선행할 것들
본격적인 테스트 코드를 작성하기 전에 선행하면 좋을 것들이 있다.
0.1. linter or/and prettier
코드 퀄리티 유지를 위해서 linter 혹은/그리고 Prettier 가 필수다.
linter 는 ESLint 와 Git 의 pre-commit hook 과 결합해 프로젝트에 적용해두었다.
Prettier 또한 vscode 의 format on save 기능을 활용해 적용해두었다.
0.2. type checking
마찬가지로 코드 퀄리티 유지를 위해 Flow 혹은 TypeScript 를 사용하면 좋다. 우리 프로젝트에선 이미 TypeScript 를 사용하고 있다.
1. 단위 테스트
1.1. 대상 및 원칙
단위 테스트는 가장 작은 단위의 함수에 대한 테스트를 의미한다. 일단 단위 테스트 코드를 작성하기에 앞서 함수들을 testable 하게 작성하는 것이 중요하다.
testable 한 코드에는 많은 특성이 있겠지만, 아래 두 특성이 제일 중요하다고 생각한다.
- 순수하다.
- 함수와 비즈니스 로직과 분리되어 있다.
모든 함수에 대해 단위 테스트를 작성할 필요는 없다.
- 일단 testable 하지 않은 코드들은 리팩토링이 선행되어야 할 것이다.
- 특정 모듈에 대해 사용자가 사용할 가능성이 없는 메서드 (예를 들면 private 멤버) 도 생략해도 된다.
- 처음부터 커버리지 백퍼센트를 노리기보다는 함수의 크기/중요도에 따라 단계적으로 적용하는 게 좋다.
1.2. 네이밍 컨벤션
각 테스트 케이스에 대해 이름을 잘 짓는 것도 중요하다. 보편적인 네이밍 컨벤션으로는 "It Should" 형식 혹은 "Given/When/Then" 형식이 있다. 각 컨벤션에 대해서 어떤 형식이 좋을지는 아래 글들을 참고하자.
- Naming Your Unit Tests: It Should vs. Given/When/Then
- GivenWhenThen
- A guide to unit testing in JavaScript
(개인적으로 "It Should" 형식을 선호한다.)
1.3. 툴
"javascript test framework" 혹은 "javascript unit test" 라고 구글링하면 많은 프레임워크가 검색된다. 그 중 검색순위가 높기도 하면서 React 및 React Native 에 기본적으로 내장되어있는 Jest 를 사용할 예정이다.
1.4. 적용 방안
일단 testable 하면서 단순한 유틸성 함수들을 대상으로 적용해 볼 예정이다. 네이밍은 "It should" 방식으로 한다. 오늘 언급하는 테스트 중 유일하게 경험이 있는 테스트이므로 어려움이 있을 것 같지는 않다.
2. React 테스트
React 에서는 테스트를 할 항목이 크게 두 가지가 있는 것으로 보인다. 하나는 커스텀 hooks, 다른 하나는 컴포넌트다.
2.1. Custom Hooks
React hooks 는 testable 한 코드가 아니다. closure 를 활용하면서 dependency 에 의한 사이드 이펙트를 일으키는 함수다. 따라서 일반적인 단위 테스트 코드로는 테스트가 어렵다.
다행히도 React hooks 를 위한 테스팅 라이브러리가 존재한다 (react-hooks-testing-library). 해당 라이브러리에서는 hook 테스팅 원칙을 아래처럼 제안한다.
언제 이 라이브러리를 사용하는가
- 당신이 컴포넌트와 직접적으로 연결되지 않은 커스텀 훅을 작성했을 때
- 당신이 작성한 훅이 컴포넌트의 인터렉션 테스트만으로는 테스트하기 어려울 때
언제 이 라이브러리를 사용하지 않는가
- 당신이 작성한 훅이 컴포넌트와 나란히 정의되어있고 그 컴포넌트에서만 쓰일 때
- 당신이 작성한 훅을 사용한 컴포넌트를 테스트하는 것으로 당신의 훅이 테스트될 수 있을 때
이러한 원칙대로라면 사실 테스트할 hook 이 많지 않긴 하다. 그래도 있기는 하므로, 살펴보고 작성해 볼 가치는 있다.
2.2. 컴포넌트
컴포넌트를 테스트하기 위한 관점은 두 가지가 존재한다. 하나는 컴포넌트를 사용하는 사용자의 관점에서 하는 interaction 테스트, 다른 하나는 어떻게 렌더링되는지에 대한 render 테스트다.
2.2.1. interaction 테스트
interaction 테스트는 어디까지나 사용자의 사용 관점에서 테스트를 한다. 따라서 state 혹은 props 값에 대한 테스트는 하지 않고 사용자가 보는 것과 발생할 이벤트에 대해 초점을 맞춰서 진행한다.
아래 예제 코드(출처)를 보면 대략 감이 올 것이다.
test("given empty GroceryShoppingList, user can add an item to it", () => {
const { getByPlaceholder, getByText, getAllByText } = render(
<GroceryShoppingList />
);
fireEvent.changeText(getByPlaceholder("Enter grocery item"), "banana");
fireEvent.press(getByText("Add the item to list"));
const bananaElements = getAllByText("banana");
expect(bananaElements).toHaveLength(1); // expect 'banana' to be on the list
});
이 테스트 코드는 Jest 에서는 기본적으로 제공하지 않기 때문에, React Native Testing Library 를 써야만 한다.
2.2.2. render
render 테스트는 컴포넌트가 렌더링된 결과에 대한 테스트다. 그런데 그 테스트 방식이 다소 당황스럽다.
The snapshot test then creates a snapshot and saves it to a file in your repo as a reference snapshot. The file is then committed and checked during code review. (출처: Testing Rendered Output)
A typical snapshot test case renders a UI component, takes a snapshot, then compares it to a reference snapshot file stored alongside the test. (출처: Snapshot Testing)
정리하자면 컴포넌트의 렌더링 결과를 jsx-like 문자열로 생성한 뒤에 해당 문자열을 미리 작성해 둔 jsx-like 문자열과 비교하거나 동료에게 코드 리뷰 요청해야 한다는 것이다.
일단 개인적으로 "코드 리뷰를 요청"하는 방법은 테스트 코드의 범주를 벗어난다고 생각된다. 따라서 사용하지 않는다.
"미리 작성해 둔 jsx-like 문자열과 비교"하는 방법 또한, 변화가 잦은 프로젝트 초기에 도입할 내용은 아니라고 생각한다. 따라서 render 테스트도 당장은 도입하지 않을 것이다.
도입하지는 않지만 나중에 생각이 바뀔 수도 있으므로, 어떤 툴을 써야하는지는 정리해두자. render 테스트는 Jest 와 react-test-renderer 의 조합으로 작성할 수 있다.
2.2.3. mock-up
데이터 목업 또한 필요하다. 데이터 독립적인, 비즈니스 로직에 대해 독립적인 컴포넌트를 테스트할 때야 문제 없지만, 모든 컴포넌트를 데이터 독립적으로 작성할 수는 없다. fetch 콜 등 데이터와 결합된 컴포넌트도 테스트할 때가 올 것이다.
다행히 멀리 갈 것 없이 React Testing Library에서 목업을 제공한다.
2.3. 적용 방안
React 테스트는 hooks 테스트와 interaction 테스트를 적용해볼 예정이다. render 테스트는 당장 염두에 두지 않는다.
3. API 콜 테스트
API 콜에 대한 테스트 코드를 작성해보고 싶은 마음이 있다. 하지만 이 테스트를 프론트엔드에서 하는 것이 맞는지 의문이다. 백엔드 엔지니어가 당연히 각각의 API 콜에 대한 테스트 코드를 작성할텐데, 프론트엔드에서 똑같은 API 콜에 대해 테스크 코드를 작성한다면 단순히 코드 중복일 뿐이기 때문이다. 거기다가 프론트엔드의 테스트 때문에 API 콜 요청 수가 늘어나면 서버 리소스도 낭비된다.
"javascript fetch testing" 으로 구글링하면 fetch 테스트에 대한 내용은 거의 없고 뷰/컴포넌트 테스트를 위해 fetch 를 mock-up 하는 방법들이 대부분인 것도 이런 이유가 아닐까 싶다.
일단 구글링도 더 해보고 백엔드 엔지니어 분들의 의견도 들어봐야 할 것 같지만, 작성하지 않을 가능성이 크다.
4. 통합 테스트
통합 테스트는 앱의 여러 부분이 같이 동작하는 것을 테스트하는 것을 의미한다. 말그대로 사용자 관점에서 테스트를 한다고 생각하면 이해하기 쉽다. 예를 들면 "사용자가 사이트에 접속해 로그인을 완료하기까지의 과정"을 하나의 시나리오로 잡고 테스트 케이스를 만들 수 있다.
통합 테스트를 직접 해 본 적은 없지만 몇 년 전 상급자가 사용하는 것을 본 적은 있다. Selenium과 Capybara를 조합해서 쓰셨던 걸로 기억하는데, 오랜 시간이 지나서 잘 기억나지 않음에도, 상급자가 매우 고생했던 것은 기억에 남는다. 테스트 코드를 구성하는 것도 쉽지 않고, Selenium 이 내부적으로 브라우저 엔진을 띄워서 그 위에서 테스트 코드를 실행했으므로 테스트 실행 시간 또한 짧지 않았다.
좋은 테스트 시나리오를 만들기도 쉽지 않은 데다가, 대부분의 프로 덕트는 살아있으므로 업데이트를 할 때마다 시나리오가 (조금이라도) 바뀌는 경우가 많아 유지보수하는데도 신경 쓸 것이 많았던 걸로 기억한다.
일단 다른 테스트들이 성공적으로 도입된 이후에, 그리고 프로젝트가 어느정도 안정기에 들어선 다음에 도입을 고려해야겠다.
5. 기타 테스트
추가적으로 신경써야 할 테스트들은 아래와 같다.
- Redux store
- routing 혹은 navigation
이 항목들은 사용하는 라이브러리에 따라 테스트 코드 작성 방식도 변경될 수 있으므로 추가적인 리서치가 필요할 것으로 보인다. 최소한 2. React 테스트 정도는 끝낸 뒤에야 도입을 고려할 듯 싶다.
결론
일단 1. 단위 테스트와 2. React 테스트에 중점을 두고 테스트를 도입해 볼 예정이다. 두 항목에 대해 어느정도 궤도에 올랐을 때 5. 기타 테스트에서 언급한 테스트들을 살펴볼 것이다. 3. API 콜 테스트와 4. 통합 테스트에 대해서도 계속 알아보긴 하겠지만, 도입하지 않거나 최후에야 도입할 것 같다.
- 단위 테스트: 도입 중
- React 테스트: 도입 예정
- API 콜 테스트: 도입하지 않을 가능성이 높음
- 통합 테스트: 도입한다면 가장 마지막에
- 기타 테스트: 1,2번이 어느정도 도입되면 도입 시작
도입 과정은 블로그에 계속해서 남겨 볼 생각이다.
참고 자료
다른 글들도 참고하긴 했지만, 아래 두 문서 (및 두 문서에 연결된 문서들) 의 내용을 주로 참고해 작성하였다.