유틸 테스트를 위한 테스트 코드 도입기

리브레리브레
7 min read

참여하고 있는 프로젝트 내에서 테스트 코드 도입에 대한 필요성이 몇 개월 전부터 대두되었다. 공용으로 쓰고 있는 유틸들이 점점 늘어남에 따라 이 유틸들이 의도한 대로 동작하는지를 확인하기 위해 테스트 코드 라이브러리가 있으면 좋겠다는 게 공통의 의견이었는데, 각자의 업무로 인해서 도입은 계속해서 늦어지게 되었다.

그러던 중에 공용 유틸이 적용될 폼 필드를 하나 만들어야 하는 기회가 최근 생겼고, 이 기회를 놓치면 또 질질끌 것 같아 이번에 테스트 코드를 도입하겠다는 이야기를 꺼내고 실천했다.

어떤 테스트 코드 라이브러리가 좋을까?

React에서 쓰이는 테스트 코드라면 보통 Vitest, Jest, React Testing Library가 생각난다.

그 밖에도 PlayWright라던지, Cypress 도 있긴 한데 뭘 쓸지 고민하던 당시엔 위의 세 가지만 떠올라서 어떤 걸 쓸지 여러모로 다른 사람의 블로그나 공식 문서 등을 조사하고 정리한 뒤, GPT한테 요약해달라고 했다.

항목JestVitest@testing-library/react
패키지 용량5.01MB1.56MB337KB
성능 / 속도상대적으로 느림빠름 (Vite와 통합)테스트 러너에 따라 다름 (주로 Jest 또는 Vitest와 함께 사용)
기능성올인원 솔루션 (테스트 러너, assertion, mocking, 스파이 등)빠르고 경량화된 테스트 러너, Jest와 유사한 기능 제공UI 테스트에 특화, DOM 상호작용 중심
커뮤니티 및 지원매우 활발하고 큰 커뮤니티, 문서 풍부빠르게 성장하는 커뮤니티, Vite와 밀접한 연관React 생태계에서 표준으로 널리 사용, 커뮤니티 활발
설정 및 사용 편의성기본 설정으로 많은 기능 제공, 간단한 설정Vite와의 통합 덕분에 빠르게 설정 가능React 환경에서 간단하게 설정 가능, Jest나 Vitest와 함께 사용
테스트 환경 / 호환성Node.js 및 다양한 브라우저 환경 지원Vite 기반의 프로젝트에 최적화, jsdom 지원Jest, Vitest와 호환, DOM 기반 테스트
현재 관리 여부2달 전 배포5일 전 배포2달 전 배포
주요 사용 사례전체 프로젝트의 통합 테스트Vite 프로젝트에서 빠른 테스트 실행React 컴포넌트 UI 테스트

React Testing Library는 다른 테스트 코드 라이브러리와 함께 쓰이는 느낌이라 배제하고 사실상 많이 쓰이는 Jest와 Vitest였는데, 고민 끝에 Vitest를 쓰기로 했고 모두 동의했다.

프로젝트에서 Vitest를 선택한 이유?

Vitest를 선택한 데에는 몇 가지 요인이 있었다.

  1. 우선, 계속해서 잘 관리되고 있는 라이브러리이다.

    다른 라이브러리도 활발하게 관리되고 있지만 고민하던 당시의 최근 publish가 제일 가까운 날짜였다.

  2. Jest에 비해서 경량화된 패키지라는 점도 한몫했다.

    5MB가 넘는 Jest에 비해 Vitest는 1.59MB 정도 된다. 아울러 Jest에 비해 속도 면에서도 3배 이상 빠른 성능을 보여준다는 점도 강점이 됐다.

  3. JSDOM을 지원해준다.

    JSDOM이란, node.js에서 브라우저 환경을 완벽하게는 아니지만 DOM 요소를 테스트할 수 있도록 도와주는 라이브러리이다. Vitest에서는 JSDOM을 지원해주기 때문에 브라우저를 열지 않더라도 브라우저에서 확인할 테스트를 어느 정도 보완해준다는 장점이 있다. (다만, 위에도 말했지만 완벽하게는 아니다.)

  4. vite와 같은 환경 설정을 공유한다.

    이게 생각보다 큰 포인트인데, 프로젝트의 번들 환경이 webpack에서 vite로 변경되었다.

    프로젝트 특성상 모노레포이고, 최상위 프로젝트가 vite로 변경됨에 따라 호환성을 고려해 하위 프로젝트 또한 vite로 환경을 맞춰야했는데 이 vite에서 설정하는 vite.config.mts 파일에서 Vitest의 설정을 함께 적용할 수 있다는 점은 우리에게 있어 매우 큰 장점이었다. (참고로 최상위 프로젝트가 vite로 변경하게 된 가장 큰 이유는 빌드 시간이었다고 한다.)

Vitest를 설치하고 첫 테스트 코드 작성해보기

자, 그럼 백문이 불여일견이라고 우선 Vitest를 설치부터 해보자.

# npm
npm install -D vitest

# yarn
yarn add -D vitest

# pnpm
pnpm add -D vitest

# bun
bun add -D vitest

설치가 완료됐다면, 이제 제작해놓은 유틸을 테스트하기 위해 테스트 코드 파일을 작성해보자. (파일명).test.ts로 생성해주고, 생성되었다면 이제 테스트 코드를 작성해보자.

아래는 개인 프로젝트에서 가져온 예시 코드이다.

export const convertDateToFormat = (
  timeStamp: Date,
  division: string,
  viewTime?: boolean
) => {
  const convertedDate = new Date(timeStamp);

  const year = convertedDate.getFullYear();
  const month =
    convertedDate.getMonth() + 1 < 10
      ? `0${convertedDate.getMonth() + 1}`
      : convertedDate.getMonth();
  const date =
    convertedDate.getDate() < 10
      ? `0${convertedDate.getDate()}`
      : convertedDate.getDate();
  const hour =
    convertedDate.getHours() < 10
      ? `0${convertedDate.getHours()}`
      : convertedDate.getHours();
  const minute =
    convertedDate.getMinutes() < 10
      ? `0${convertedDate.getMinutes()}`
      : convertedDate.getMinutes();

  return `${year}${division}${month}${division}${date}${
    viewTime ? ` ${hour}:${minute}` : ""
  }`;
};
import { expect, test } from 'vitest'
import { convertDateToFormat } from "./format-date";

test("테스트 1: 시간 없이 날짜만 포맷", () => {
  const date = new Date("2025-03-24T13:09:00Z");
  const formattedDate = convertDateToFormat(date, "-", false);
  expect(formattedDate).toBe("2025-03-24");
});

test("테스트 2: 날짜와 시간 포함", () => {
  const date = new Date("2025-03-24T13:09:00Z");
  const formattedDate = convertDateToFormat(date, "/", true);
  expect(formattedDate).toBe("2025/03/24 13:09");
});

test("테스트 3: 한 자리수 월과 날짜 처리", () => {
  const date = new Date("2025-01-05T02:03:00Z");
  const formattedDate = convertDateToFormat(date, ".", true);
  expect(formattedDate).toBe("2025.01.05 02:03");
});

test("테스트 4: 다양한 분리 기호 처리", () => {
  const date = new Date("2025-12-31T23:59:00Z");
  const formattedDate = convertDateToFormat(date, "_", false);
  expect(formattedDate).toBe("2025_12_31");
});

작성을 완료했다면 package.json을 열어 script 항목에 다음과 같은 내용을 추가하고 저장해주자.

"scripts": {
  "dev": "vite",
  "build": "tsc -b && vite build",
  "lint": "eslint .",
  "preview": "vite preview",
  "test": "vitest"
},

저장하고 완료했다면 이제 터미널에 명령어를 입력해 테스트 결과를 확인해보자.

# npm
npm run test

# yarn
yarn run test

# pnpm
pnpm run test

# bun
bun run test

그럼 터미널에서 아래와 같이 결과를 확인할 수 있다.

 DEV  v3.0.9 

 ❯ src/utils/format-date.test.ts (4 tests | 3 failed) 8ms
   ✓ 테스트 1: 시간 없이 날짜만 포맷
   × 테스트 2: 날짜와 시간 포함 5ms
     → expected '2025/03/24 22:09' to be '2025/03/24 13:09' // Object.is equality
   × 테스트 3: 한 자리수 월과 날짜 처리 1ms
     → expected '2025.01.05 11:03' to be '2025.01.05 02:03' // Object.is equality
   × 테스트 4: 다양한 분리 기호 처리 1ms
     → expected '2026_01_01' to be '2025_12_31' // Object.is equality

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 3 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯

 FAIL  src/utils/format-date.test.ts > 테스트 2: 날짜와 시간 포함
AssertionError: expected '2025/03/24 22:09' to be '2025/03/24 13:09' // Object.is equality

Expected: "2025/03/24 13:09"
Received: "2025/03/24 22:09"

 ❯ src/utils/format-date.test.ts:13:25
     11|   const date = new Date("2025-03-24T13:09:00Z");
     12|   const formattedDate = convertDateToFormat(date, "/", true);
     13|   expect(formattedDate).toBe("2025/03/24 13:09");
       |                         ^
     14| });
     15|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/3]⎯

 FAIL  src/utils/format-date.test.ts > 테스트 3: 한 자리수 월과 날짜 처리
AssertionError: expected '2025.01.05 11:03' to be '2025.01.05 02:03' // Object.is equality

Expected: "2025.01.05 02:03"
Received: "2025.01.05 11:03"

 ❯ src/utils/format-date.test.ts:19:25
     17|   const date = new Date("2025-01-05T02:03:00Z");
     18|   const formattedDate = convertDateToFormat(date, ".", true);
     19|   expect(formattedDate).toBe("2025.01.05 02:03");
       |                         ^
     20| });
     21|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/3]⎯

 FAIL  src/utils/format-date.test.ts > 테스트 4: 다양한 분리 기호 처리
AssertionError: expected '2026_01_01' to be '2025_12_31' // Object.is equality

Expected: "2025_12_31"
Received: "2026_01_01"

 ❯ src/utils/format-date.test.ts:25:25
     23|   const date = new Date("2025-12-31T23:59:00Z");
     24|   const formattedDate = convertDateToFormat(date, "_", false);
     25|   expect(formattedDate).toBe("2025_12_31");
       |                         ^
     26| });
     27|

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[3/3]⎯

 Test Files  1 failed (1)
      Tests  3 failed | 1 passed (4)
   Start at  22:14:08
   Duration  296ms (transform 64ms, setup 0ms, collect 41ms, tests 8ms, environment 0ms, prepare 91ms)

 FAIL  Tests failed. Watching for file changes...

위처럼 Fail이 나온다면 이제 코드가 테스트를 넘어가지 못 한 거니, 수정을 계속 진행해 테스트 결과가 전부 Success가 되도록 고쳐나가면 되시겠다.

물론 Vitest의 코드가 test와 expect만 가지고 작성하는 건 아니다. 다양한 기능들이 제공되는 만큼 더 알아보고 쓰도록 하자. (요거도 조만간 글 쓸 기회가 있다면 쓰도록 하겠다.)

Vitest의 Config로 어떤 걸 설정하면 좋을까?

그래서 일단 설치는 했고, 사용도 해봤는데 그냥 이렇게 마냥 설치해두고 두면 되는 것인가…

그건 또 아닌 거 같아서 Vitest의 환경 설정 중에 어떤 것들을 배치하면 좋을지 공식 문서 등을 뒤져보고 그 중 몇 가지를 정리해봤다.

export default defineConfig({
  test: {
    globals: true,
    environment: 'jsdom',
    coverage: {
      reporter: ['text', 'json', 'lcov'],
      exclude: ['node_modules/', 'dist/', 'tests/setup.ts'],
    },
    setupFiles: ['./tests/setup.ts'], 
    testTimeout: 5000,
  },
});
  • globals

    • Jest처럼 전역으로 사용할 API를 적용할 것인지를 설정할 수 있다.
  • environment

    • jsdom 등의 환경을 적용할 것인지를 결정할 수 있다.
  • coverage

    • 테스트를 진행할 범위, 그리고 그 도출되는 결과와 관련한 항목이다.

    • reporter

      • 테스트 결과를 어떠한 형식으로 출력할 지를 결정한다.
    • include, exclude

      • 리포트 파일에서 진행할 범위에 포함할 파일, 제외할 파일들을 결정할 수 있다.
        Vitest는 기본적으로 ~test.ts, ~.spec.ts를 지원해주는데 파일들을 추가/제외하고 싶다면 여기서 해당 경로들을 작성해주면 리포트에 추가/제외시킬 수 있다.
  • setupFiles

    • 테스트 시행 전에 특정 설정 파일을 로드할 필요가 있는 경우, 파일 경로를 배열에 담아 사용한다.

프로젝트에서는 많은 옵션을 적용할 필요는 없다고 생각해서 가볍게 globalstrue로하고 environmentjsdom으로 설정하는 것으로 끝냈다.

단, globals를 true로 할 경우 전역으로 사용하게 되면서 테스트 표현 시에 describe에 있는 it으로 test 함수를 대체할 수 있는데 팀 내에서는 명시적으로 표현하는 걸 중요시하기 때문에 it을 지양하고 test를 쓰기로 했다.

Vitest가 도입된 후, 프로젝트는?

이렇게 Vitest를 도입하게 됐고, 현재는 기존에 사용하는 유틸들과 관련해 시간나는 대로 틈틈이 테스트 코드를 작성하고 있다.

팀에서는 애초에 테스트 라이브러리를 긍정적으로 검토하고 있었지만, 전체를 한꺼번에 바꾸기는 어려운 만큼 이런 유틸과 같은 작은 단위부터 하거나 혹은 신규 컴포넌트나 훅을 만들 때에 테스트 코드를 붙이자고 결정되었다.

그리고 시간이 여유로울 때(가 있을진 모르겠지만(…)) 기존의 코드들에도 테스트 코드를 붙이면서 최종적으로는 모든 로직에 테스트 코드가 심어지는 것이 최종 목표가 될 듯 하다.

얼마나 오래 걸릴 지는 모르겠지만, 시작이 반인 만큼 이참에 테스트 코드 작성에 익숙해지면서 습관을 들여보도록 노력해보자.

0
Subscribe to my newsletter

Read articles from 리브레 directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

리브레
리브레

2년차 프론트엔드 개발자입니다. 웹(React, Next.js)과 웹뷰, 앱(React Native) 개발 경험이 있어 플랫폼에 구애받는 개발이 가능합니다. 선 개발 후 개선에 따른 빠른 개발을 지향하고, 여러 번 QA와 테스트를 통해 기능을 개선합니다. 작업 경과를 중간, 완료 때마다 공유하고 논의해 소통 에러와 기능의 문제점을 최소화하고 있습니다.