이번에 맡은 프로젝트에서 테스트 코드를 도입했다. 빠르게 흘러가는 개발 일정속에 우다다다 테스트 코드를 짜다보니 프로젝트가 마무리될 즈음 구조가 보이고, 처음에 이해하고 시작했으면 좋았겠다 싶은 부분들도 왕왕 있었다.
이번에 배운것들을 잊지 않기 위해 작성하는 것도 있고, 테스트코드를 짜다가 길을 잃은것 같을 때 이 글을 만나 도움이 되었으면 좋겠다.
왜 E2E 테스트가 아닌 unit(통합)테스트를 작성했나
이전에 백오피스 프로젝트를 맡았을 땐 E2E 테스트 라이브러리인 cypress를 사용했었다. E2E 테스트는 실제 브라우저 환경에서 테스트를 하는것이고, Jest+react-testing-library 조합은 가상 DOM에서 테스트를 진행한다는 차이점이 있다.
백오피스에서는 서버에서 받은 데이터를 단순히 뿌려주는 역할을 하기 때문에, 복잡한 비즈니스 로직이랄게 없었다. 전체적인 플로우가 잘 동작하는것이 더 중요했기 때문에 E2E가 적합했다고 할 수 있다.
이번에 적용했던 프로젝트는 더 복잡한 비즈니스 로직과 자주 변경되는 특성이 있었다. 이럴 때 일수록 많이 변하지 않는 작은 단위의 테스트를 해야 많은 변경들 속에서 컴포넌트들을 지킬 수 있다.
왜 해당 조합으로 테스트를 작성했을까?
Jest 와 react-testing-library는 대중적으로 많이 사용하는 테스트 조합이다.
Jest는 페이스북에서 만든 JavaScript 테스팅 프레임워크이고, react-testing-library(이하 RTL)는 React DOM을 테스트 하는 라이브러리이다.
실제로 테스트를 작성할때는 Jest와 RTL을 섞어서 쓰기 때문에, 처음 두 조합을 함께 쓰려니 어디가 어느 문법인지 헷갈렸다.
간단히 Jest를 알아보자
Jest
function sum(a, b) {
return a + b;
}
function sumOf(numbers) {
let result = 0;
numbers.forEach(n => {
result += n;
});
return result;
}
// 각각 내보내기
exports.sum = sum;
exports.sumOf = sumOf;
이런 함수가 있다고 했을 때 jest 코드는 아래와 같이 짤 수 있다.
describe('sum', () => {
it('calculates 1 + 2', () => {
expect(sum(1, 2)).toBe(3);
});
it('calculates all numbers', () => {
const array = [1, 2, 3, 4, 5];
expect(sumOf(array)).toBe(15);
});
});
- it('',()=>{}) : 새로운 테스트 케이스를 만드는 함수이다. `test` 라고 동일한 기능을 하는 함수도 있다. 근데 왜 it 이냐면, 이렇게 쓰면 영어로 말이 되게 쓸 수 있다. (it calculates 1+2 = '1+2를 계산한다' 처럼)
- describe('',()=>{}) : it 같은 테스트 케이스들을 묶을 수 있다.
- expect() : A값이 B일 것이다 라고 기대(예상)하는 부분의 비교하는 A 부분을 담당한다.
- toBe(toBe*)(): matchers라고 하는 함수라고하며 기대(예상)하는 B값을 작성한다.
React-Testing-Library
https://testing-library.com/docs/
Introduction | Testing Library
The @testing-library family of packages helps you test UI components in
testing-library.com
리액트 테스팅 라이브러리를 사용하면서 컴포넌트를 렌더링 하고, 필요한 문구들을 선택 할 수 있다
server.use( createMSWHandler('get', MY_API ));
jestRender(<MyComponent />); //react-testing-library
it('Standard 텍스트가 화면에 표시된다', async () => {
const Element = await screen.findByTestId('title'); // react-testing-library
expect(Element).toHaveTextContent('Standard');
});
실제로 작성한 부분을 각색해 보았다.
처음에는 msw를 사용해 특정한 api 를 불러 온 후, `jestRender` 를 사용해서 컴포넌트를 렌더링했다.
테스트코드를 작성하는 부분에는 Element를 RTL의 `findByTestId`를 사용해서 찾는다.
다음은 많이 사용했던 RTL 쿼리이다
- findByText
- findByRole
- findByTestId
- getByText
- getByRole
- getByTestId
- queryByRole
- queryByText
어떤 부분이 어려웠나?
이건 예전 E2E테스트 라이브러리를 쓸때도 항상 하는 고민이지만, '어떤 기준으로 테스트를 나누어야 하는가?' 가 어려웠다. 머리에 힘을 주지 않으면, E2E 테스트처럼 모든 경우의 수를 테스트 하는 나를 볼 수 있었다. 테스트 코드 짜는 시간은 시간대로 길어지고, 가장 중요한건 지루했다...
팀원들과 어떻게 테스트 스펙을 짜면 좋을지 이야기를 나눌 시간이 있었다. 테스트 스펙 작성에 대한 고민과 함께.. 애초에 컴포넌트가 테스팅하기에 유리하지 않게 짠 것인가? 라는 고민으로 이어졌다. 어떤 기준이 있었으면 좋겠다..! 싶었다. 동료들이랑 고민하면서 내 나름대로 정리한 기준을 적어본다.
- 플로우를 테스트 하는 것이 아닌 단위 테스트를 한다.
무슨 당연한 소리를 하나 싶지만 ..
A라는 기능의 테스트 코드를 짠다고 생각해보자. A기능은 사용자의 a를 확인해서, b의 값에 따라 c를 렌더링 한다. a,b의 종류가 각각 2가지고, c의 값이 3가지라면 여기서 나올 수 있는 모든 경우의 수는 8가지가 된다.
8가지의 모든 경우의 수를 나누어 테스트 스펙을 작성하는 것보단, 각 컴포넌트가 나올 수 있는 테스트를 진행한다면 3가지의 유닛 테스트만 작성하면 된다.
- 위 처럼 a-> b -> c 의 단계가 연속적인 진행 동작을 테스트 해야 한다면, 컴포넌트 or 함수를 분리할 수 있는지 생각해보자.
코드의 냄새가 난다..! 함수형 코딩에서도 나온 말이지만 많은 Action을 작은 Action, Calculation(===함수), Data(===상수)로 분리할 수 있는지 확인해보자.
'frontend' 카테고리의 다른 글
웹서버 통신방법과 HTTP 클라이언트 라이브러리(feat. Axios, ky) (0) | 2025.04.02 |
---|