프론트엔드 테스트 표류기

2023.07.23

목차

• 서문
• 시작
• 목표 설정
• 표류
• 도착

 

서문

개발자들에게 테스트 코드는 작성하고 싶어하지만
 
1 진입 장벽이 있어서
2 예전에 작성한 적이 있지만 그 당시 작성함에 있어 피로감을 느꼈거나
3 테스트 코드를 조금만 신경쓰지 않는다면 레거시 코드가 되어버리는 경험을 했던

 
것과 같이 모종의 이유로 도입을 하지 못하고 있다고 생각하고 저 또한 경험했습니다.

 
현재 팀에서 이전에 테스트 코드를 도입을 시도했다가 실패했습니다. 실패 이유는 팀원들에게 테스트 코드에 대한 공감을 충분히 얻지 못했고 저 또한 프로덕션 코드에 대한 생각에 테스트 코드에 대해 무신경했습니다.

하지만 프로젝트 규모가 점차 커질 수록 사이드 이펙트로 인한 버그들이 생겨나고 백엔드 응답에 오류가 있음에도 곧바로 인지하지 못하고 시간이 흘러서 인지하게 되는 경우가 생겨났습니다. 이런 이슈가 발생할 때마다 테스트 코드에 대한 필요성이 대두되었습니다.

그래서 충분한 공감은 얻었다고 생각했고 테스트 코드 작성은 최대한 쉽고 명확하게 작성하였습니다.

 
 

읽고 가면 좋은 글들

Testing Implementation Details
A compilation of outstanding testing articles (with JavaScript)
뭣? 딸깍 몇 번에 웹 E2E 테스트 코드를 생성하고 수행한다고? 에러도 잡아준다고? 영상도 뽑아준다고?

 
 

알고 가면 좋은 것들

  • E2E 테스트란? - 실제 유저의 시작부터 끝까지의 행동을 브라우저에서 하는 테스트입니다.
  • 단위 테스트란? - 테스트가 가능한 가장 작은 단위에 대한 테스트입니다.
  • 통합 테스트란? - 두 가지 이상의 요소가 함께 상호 작용할 때, 프론트 환경에서는 하나 혹은 여러개의 상태에
    종속된 컴포넌트를 테스트하는 경우에 대한 테스트입니다.
  • Cypress란? - 자바스크립트 E2E 테스트 도구 중 하나입니다. Cypress에 대한 좋은 글로 대체합니다.
  • Jest란? - 자바스크립트 단위, 통합 테스트 도구 중 하나입니다. Jest에 대한 좋은 글로 대체합니다.

 
 

시작

테스트를 하기 위한 모듈을 설치하기 전에 가설을 세웠습니다.
 

프론트엔드에서 E2E 테스트 하는 것은 쉽지 않을 것이다. >  
왜냐하면 기획이나 디자인이 변경되어 구현해야 하는 화면이 변경되는 순간 E2E 테스트 코드는 깨질 것이기 때문입니다. 그래서 E2E 테스트보다는 단위 테스트나 통합 테스트가 적합할 것이라고 생각하였습니다. 그래서 testing-library를 통해서 컴포넌트 테스트와 비즈니스 로직 테스트를 통해서 목표에 도달할 수 있을거라고 생각했습니다.

그래서 테스트 환경은 Jest를 통해 구축하고 테스트 메서드들은 testing-library를 활용해서 시작했습니다.

그리고 테스트 코드 작성에 있어 원칙을 세웠습니다.
 
첫번째는 명확성

• 테스트 코드는 명확해야 합니다. 누군가 테스트 코드를 읽을 때, 해당 테스트가 무엇을 하는지 쉽게 이해할 수 있어야 합니다.
• 테스트 코드는 input과 output에 집중하여 테스트하는 것이 목적이지 테스트 로직을 구현하는게 목표가 아닙니다. 그래서 코드가 복잡할 이유가 없습니다.
 
두번째는 독립성

• 각 테스트는 독립적으로 실행될 수 있어야 합니다. 테스트 간에 상태가 공유되면 해당 테스트는 순수한 테스트라고 할 수 없습니다.
• 테스트를 할 함수가 아닌 다른 로직(라이브러리, API)에 의존성을 가지는 경우는 해당 로직은 모킹을 활용합니다.

 
 

목표 설정

테스트 코드를 작성하기 전 테스트를 통해 얻고자 하는 것, 목표에 대해 정의하였습니다.
 
첫번째는 애플리케이션이 요구 사항에 맞게 동작하는지에 대한 검증입니다.

제일 중요한 목표입니다. 기능이 요구 사항에 맞게 동작하는지 테스트합니다. 그리고 예상 가능한 유저 플로우를 설계하고 엣지 케이스에 대해서도 테스트합니다.
 
두번째는 추가 기능 개발로 인한 사이드이펙트 발생에 대한 검증입니다.

만약 하나의 커스텀 훅이 두개 혹은 그 이상의 컴포넌트 내부에 위치한 경우가 상당히 많습니다. 추가적인 기능 개발로 인해 해당 커스텀 훅을 수정해야 한다면, 그리고 그 수정으로 인해서 다른 컴포넌트에서도 이전과 동일하게 동작할 것이라고 확신하기는 힘들다고 생각합니다.

 
 

표류

Jest를 통해서 테스트 환경을 구성한다는 것은 JSDOM을 통해서 DOM을 흉내낸 객체를 Node.js 실행 환경에서 테스트를 실행것을 의미합니다.
 
현재 Next.js의 _app.tsx 파일에는 많은 Provider들이 페이지를 감싸고 있습니다. 그렇다는 것은 컴포넌트 테스트를 하는 경우에 Provider들로 인해 컴포넌트 테스트에 대한 독립성이 지켜지지 않을 수 있습니다.
 

function App({Component, pageProps: {session, ...pageProps}}) { return ( <SessionProvider session={session}> <QueryClientProvider client={queryClient}> <InitI18n> <RecoilRoot> <ErrorBoundary FallbackComponent={ErrorFallback}> {/* more wrappers... */} <Component {...pageProps} /> {/* more wrappers... */} </ErrorBoundary> </RecoilRoot> </InitI18n> </QueryClientProvider> </SessionProvider> ); }

 
Pagination을 테스트하는 로직을 예시로 들어보겠습니다.
 
Pagination에 동작에 대한 테스트에는 인증과 관련된 로직은 필요하지 않습니다. 하지만 Pagination 상위에서 Auth Provider가 감싸고 있기에 Pagination 동작에 독립성을 지킬 수 없습니다. 그래서 테스트 코드 작성 원칙인 독립성을 위반하지 않기 위해 테스트할 사항과 관련되지 않은 Provider들은 모킹을 해야 합니다.
 

jest.mock("next-auth/react", () => { const originalModule = jest.requireActual("next-auth/react"); return { __esModule: true, ...originalModule, useSession: jest.fn(() => { return { data: { /* mocking data */ }, status: "authenticated", }; }), }; });

 
그래서 테스트 코드 작성 원칙인 독립성을 위반하지 않기 위해 테스트할 사항과 관련되지 않은 Provider들은 모킹을 해야 합니다.
 

it("A 페이지 페이지네이션 테스트", () => { // 1. queryClient에 mock data를 넣는다 queryClient.setQueryData(/* mock data */); // 2. A 페이지에 두번째 페이지로 이동하는 버튼이 존재하는지 테스트한다. render(<Apage />); expect(screen.getByTestId("pageNumber-2")).toBeInTheDocument(); // 3. 기존 queryKey에 page가 1인지 테스트한다. const caches = Array.from(queryClient.getQueryCache().findAll()); const queryKey = caches[0].queryKey; expect(beforeQueryKey.page).toBe(1); // 4. fireEvent로 페이지 클릭을 한다. fireEvent.click(screen.getByTestId("pageNumber-2")); // 5. queryKey의 page가 2로 변경되었는지 테스트한다. expect(queryKey.page).toBe(2); });

 
testing-library의 한계점
 
첫번째, HierachyForm 컴포넌트입니다.

HierachyForm 컴포넌트를 테스트 하기 위해서는 Drag 이벤트를 활용해야 합니다. 하지만 testing-library에서는 힘든 점이 있습니다. https://github.com/testing-library/user-event/issues/440 해당 이슈에서 testing-library 주요 컨트리뷰터 또한 Cypress가 더 적합하다고 조언해줍니다.
 
두번째, next-auth 입니다.

로그인을 next-auth를 통해서 진행하고 있습니다. 로그인 과정을 Jest에서 테스트하는 것은 많은 어려움이 예견되어 있습니다. 일단 실행되는 환경이 다릅니다. Jest는 Node.js 환경에서 동작하고 next-auth는 주로 클라이언트 측에서 동작하며, 서비 측에서는 Next.js의 API 라우트를 통해 동작합니다. Jest는 기본적으로 Node.js 환경에서 동작하기 때문에 브라우저의 특정 API나 환경을 직접 사용할 수 없는 문제가 있습니다. 또한 Jest에서 next-auth 같은 서드파티 라이브러리는 mocking을 통해 원하는 값을 반환하게는 할 수 있지만 실제 인증 프로세스는 테스트 할 수 없다는 것입니다.
 
위와 같은 문제 때문에 목표인 요구 사항에 맞게 동작하는지를 검증 이 힘들다는 것입니다.

 
 

도착

달성해야하는 목표는 요구 사항에 맞게 동작하는지를 검증 입니다. 그래서 처음 세웠던 가설을 부수고 cypress의 E2E 테스트를 프로젝트에 적용해보았습니다. cypress는 npm startyarn start 와 동일한 방식으로 서버를 띄워 테스트를 진행하기에 위에서 발생했던 next-auth를 통한 로그인 프로세스 테스트가 굉장히 수월하였습니다.
 
또한 cypress에서 컴포넌트 테스트를 위한 Cypress Component Testing 이라는 기능을 제공합니다. 이를 통해 Drag를 하야 테스트해야 하는 HierachyForm 컴포넌트도 테스트할 수 있었습니다.
 
그리고 cypress에서는 사용자 액션(클릭, 입력 등등)을 통해 테스트 코드를 제너레이트하는 기능인 cypress studio를 제공합니다. 이를 통해 개발자가 더 쉽고 빠르게 테스트를 작성할 수 있습니다. 그리고 정확한 기능 동작, 기능에 대한 버그를 잡기 위해 작성하는 테스트 코드를 잘못 작성하는 경우 그 문제를 해결하기 위해서는 더 많은 시간이 걸릴 것입니다. 그래서 사람이 직접 작성하는 테스트 코드의 양을 적게 할 수 있는 장점이 있습니다.
 
도착한 이 섬이 정답이 아닐 모르지만 달성하고자 하는 목표에 도달했다고 생각합니다. 그리고 테스트를 좀 더 빠르고 정확하게 작성할 수 있는 기능 또한 저희의 생산성에 도움이 될 것으로 생각합니다.