back

Posts

사내 테스트코드 도입기

2024-01-05

이제는 테스트 코드를 작성할 때

작년 5월 이직 후, 회사에서 담당한 프로젝트를 지속적으로 리팩토링 및 기능 추가를 진행하고 있었다.

사실 개발을 진행하면서 테스트코드 없이 로직검증을 위해 생각보다 많은 시간이 소요되는걸 느끼고 테스트 코드의 필요성을 깨달았다. (특정시간에 오픈되는 UI, 가격계산로직 등등... 테스트코드 없이 검증하려니 비효율적으로 낭비되는 시간과 노력이 많이 발견됨)

그러다 다른 팀의 신규 프로젝트 준비로 잠시 여유가 생겨 미루고 미뤄뒀던 테스트 코드를 도입하기로 결정했다.

Why Vitest

처음엔 jest를 사용하려 했지만 최종적으론 vitest를 사용하기로 했다. jest에 비해 초기설정도 간단하며 테스트 속도도 더 빠른 것으로 판단되어 vitest를 사용하지 않을 이유가 없었다.

또한, vitest/ui 라이브러리를 통한 GUI도 깔끔하여 개발 효율이 더 증가해서 만족스러웠다.

비록 아직 Vitest 사용자가 많지 않아 레퍼런스가 적지만, jest와 90% 이상(주관적 수치입니다)유사하여 막히는 부분이 있으면 ChatGPT의 도움으로도 충분히 커버가 가능하다.

Test파일은 어디에 두어야 하나

처음 테스트 코드를 작성하면서 가장 큰 고민은 테스트 코드의 위치였다. 처음엔 __tests__ 폴더를 생성하여 하위 폴더에 작성하였다.

그러나 이 방식은 테스트 코드와 파일들의 위치가 너무 멀어 매번 스크롤을 해주고, 원하는 테스트 파일을 찾는데 시간도 걸리는 문제도 있다는 단점이 있었다.

결국 개발 효율을 위해 테스트 관련 유틸 및 설정파일은 __tests__ 폴더에 두고, 나머지 파일들은 테스트대상 파일 바로 아래에 두는 방식으로 변경했다.

트러블슈팅

next/Image src문제

next/image컴포넌트의 src는 보통 "/_next/image?url=~~"와 같은 형태로 구성된다.

만약 이미지 경로가 "testImage.com"이라고 가정하면 next/image를 통한 src는 "/_next/image?url=testImage.com&w=640&q=50"과 같은 형태이므로 expect().toHaveAttribute('src',"testImage.com")는 틀린 테스트케이스가 된다.

이 문제는 next/image컴포넌트 자체를 mocking하여 일반 img태그를 변환시키는 방법으로 해결했다.

import { vi } from 'vitest';

const mockNextImage = () => {
  vi.mock('next/image', () => ({
    __esModule: true,
    default: (
      props: React.DetailedHTMLProps<
        React.ImgHTMLAttributes,
        HTMLImageElement
      >,
    ) => {
      return {props.alt};
    },
  }));
};

export { mockNextImage };

Next.js Dynamic Import

특정 컴포넌트는 nextjs에서 제공하는 dynamic을 사용해서 import하고 있었다. 이러한 컴포넌트들은 vitest 테스트 환경에선 load되지 않는 문제가 있다.

해당 문제는 next/dynamic자체를 mocking하여 해결 가능하다.

vi.mock('next/dynamic', async () => {
  const dynamicModule: any = await vi.importActual('next/dynamic');
  return {
    default: (loader: any) => {
      const dynamicActualComp = dynamicModule.default;
      const RequiredComponent = dynamicActualComp(() =>
        loader().then((mod: any) => mod.default || mod),
      );

      // for debugging
      if (RequiredComponent?.render?.displayName) {
        RequiredComponent.render.displayName = loader.toString();
      }

      RequiredComponent.preload
        ? RequiredComponent.preload()
        : RequiredComponent.render.preload();

      return RequiredComponent;
    },
  };
});