(React) Vitest를 이용하여 테스트 코드 작성하기(+ESM과 CJS)

kidstone·2024년 4월 25일
0

외개인 프로젝트

목록 보기
3/10

문제 상황

jest와 react testing library, msw를 사용하여 테스트 코드를 작성하는 과정에서 "TextEncoder is not defined"라는 에러가 발생하였다.

FAIL  src/pages/HomePage/HomePage.test.jsx
● Test suite failed to run


ReferenceError: TextEncoder is not defined

  1 | // src/mocks/browser.js
> 2 | import { setupWorker } from 'msw/browser'
    | ^
  3 | import { handlers } from './handlers'
  4 |
  5 | export const worker = setupWorker(...handlers)

  at Object.<anonymous> (node_modules/msw/node_modules/.pnpm/@mswjs+interceptors@0.26.14/node_modules/@mswjs/interceptors/src/utils/bufferUtils.ts:1:17)
  at Object.<anonymous> (src/mocks/browser.js:2:1)
  at Object.<anonymous> (src/pages/HomePage/HomePage.test.jsx:3:1)
  at TestScheduler.scheduleTests (node_modules/@jest/core/build/TestScheduler.js:333:13)

이는 msw 2.x.x 버전으로 업그레이드 하면서 발생하는 에러로, 공식 홈페이지에도 해결 방안이 제시되어 있다.
https://mswjs.io/docs/migrations/1.x-to-2.x#requestresponsetextencoder-is-not-defined-jest
하지만 위 해결방법은 jest.config.js를 수정해야하는 작업이기 때문에 create-react-app를 사용한 이 프로젝트에서는 해결방법을 적용하기 매우 번거로웠고 많은 시간이 소요되었다. 구글링을 열심히 해본 결과, 여러 사람들이 나와 같은 문제를 겪고 있었고, msw를 버전 1로 다운그레이드 했거나, vitest와 같은 최신 테스트 프레임워크를 사용하는 것을 추천하였다.
이 프로젝트에서 craco를 이용하여 프로젝트 설정을 하고 있었기 때문에 어찌저찌해서 공식 문서의 해결방안을 따랐다. 하지만 다음과 같은 오류가 발생하였다.

export {
^^^^^^


SyntaxError: Unexpected token 'export'

> 1 | import { http, HttpResponse } from 'msw'
    | ^
  2 |
  3 | export const handlers = [
  4 |   // By calling "http.get()" we're instructing MSW

다른 개발자들이 말하길, jest는 원래부터 ESM에 좋지 않았으며 아직도 제대로 지원하지 않는 기술이라고도 하였다.

JavaScript에서 모듈을 정의하고 사용하는 두 가지 주요 방식에는 ESM과 CommonJS가 있다.

ESM이란

ESM은 ECMAScript의 공식 모듈 시스템이다. importexport 구문을 사용하여 모듈을 불러오고 내보낸다. 주로 브라우저 환경에서 사용되며, 최신 Node.js 버전에서도 지원된다.

특징

  • 정적 구조: 컴파일 시간에 모듈 구조가 결정되어 최적화에 유리
  • 비동기 로딩 지원: 모듈을 비동기적으로 로딩할 수 있어, 효율적인 코드 분할과 로딩이 가능
  • 트리 쉐이킹(Tree Shaking) 지원: 사용되지 않는 코드를 제거하여 최종 번들의 크기를 줄일 수 있음

CommonJS

CommonJS는 서버 사이드 및 데스크톰 애플리케이션을 위해 설계된 모듈 시스템이다. requiremodule.exports를 사용하여 모듈을 불러오고 내보낸다. 주로 Node.js 환경에서 사용된다.

특징

  • 동적 구조: 런타임에 모듈 구조가 결정되어, 조건부 로딩이나 동적 로딩이 가능
  • 동기 로딩: 모듈이 필요할 때까지 실행을 멈추고 모듈을 로딩한다. 이는 서버 사이드 환경에서는 큰 문제가 되지 않지만, 브라우저 환경에서는 성능 저하의 원인이 될 수 있다.

Vitest란

Vitest는 JavaScript 및 TypeScript 프로젝트를 위한 모던 테스트 러너 및 테스트 프레임워크이다. Jest처럼 테스트 실행뿐만 아니라 모킹(mocking)과 스냅샷(snapshot)을 지원하며, Jest와 호환되는 API를 제공하고 있어서 Jest 사용자들이 쉽게 전환할 수 있도록 설계되었다.

특징

  • 빠른 실행 속도: Vite의 모듈 서버와 esbuild를 활용하여 테스트 파일과 의존성을 빠르게 로드하고 실행한다.
  • Jest와 호환: Vitest는 Jest의 API와 호환되는 설정과 명령어를 제공하여 Jest 사용자들이 손쉽게 전환할 수 있게 한다. 하지만 완벽한 호환성은 보장하지 않는다.
  • 내장된 커버리지 지원: Istanbul을 통한 코드 커버리지 리포트를 내장 지원하여 추가 설정 없이 코드 커버리지를 측정할 수 있다.
  • TypeScript와 Vue 지원: TypeScript 및 Vue 파일을 별도의 플러그인이나 설정 없이 직접 테스트할 수 있다.
    병렬 테스트 실행: 테스트를 병렬로 실행하여 전체 테스트 시간을 단축시킬 수 있다.
  • Vitest는 특히 Vite, Vue, React, Svelte 등을 사용하는 프로젝트에 적합한 테스트 도구로, 개발자들이 더 빠르고 효율적으로 테스트 환경을 구축하고 관리할 수 있도록 도운다.

설치

npm install -D vitest

https://vitest.dev/guide/

npm install @vitejs/plugin-react

이는 Vite와 React를 함께 사용할때 필요한 플러그인이다. React 프로젝트에 Vite를 통합할 때 필요한 설정과 최적화를 제공한다.
https://www.npmjs.com/package/@vitejs/plugin-react

package.json

"scripts": {
    "start": "craco start",
    "build": "craco build",
    "test": "vitest",
    "test:run": "vitest run",
    "eject": "react-scripts eject"
  },

test 부분을 "vitest"로 변경해준다.

vite.config.js

import { defineConfig } from 'vitest/config'
import react from '@vitejs/plugin-react'

const path = require('path');

export default defineConfig({
  plugins: [react({
    jsxRuntime: 'automatic',
  })],
  test: {
    // ... Specify options here.
    globals: true,
    environment: "jsdom",
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src/'),
			'@components': path.resolve(__dirname, 'src/components/'),
			'@common': path.resolve(__dirname, 'src/components/common/'),
			'@assets': path.resolve(__dirname, 'src/assets/'),
			'@pages': path.resolve(__dirname, 'src/pages/'),
			'@services': path.resolve(__dirname, 'src/services/'),
			'@styles': path.resolve(__dirname, 'src/styles/'),
			'@store': path.resolve(__dirname, 'src/store/'),
			'@constants': path.resolve(__dirname, 'src/constants/'),
			'@utils': path.resolve(__dirname, 'src/utils/'),
			'@hooks': path.resolve(__dirname, 'src/hooks/'),
    }
  }
})

config 파일에는 vitest가 설정해놓은 절대경로를 읽을 수 있도록 하였고, plugins에 react를 넣어 react를 사용할 수 있도록 하였다.

test 코드 작성

import { fireEvent, render, screen } from '@testing-library/react';
import RoommateFilterPage from './RoommateFilterPage';
import { expect, test, describe } from "vitest";
import '@testing-library/jest-dom';


describe('YourComponent', () => {
  test('사용자가 인원을 선택할 때 상태가 올바르게 업데이트되어야 한다', () => {
    render(<RoommateFilterPage />);
    
    // '2인'과 '3인' 선택
    fireEvent.click(screen.getByTestId('2인'));
    fireEvent.click(screen.getByTestId('3인'));

    // '2인'과 '3인'이 선택되었는지 확인
    expect(screen.getByTestId('2인')).toBeChecked();
    expect(screen.getByTestId('3인')).toBeChecked();

    // '2인' 선택 해제
    fireEvent.click(screen.getByTestId('2인'));

    // '2인'이 선택 해제되었는지 확인
    expect(screen.getByTestId('2인')).not.toBeChecked();
    // '3인'은 여전히 선택되어 있는지 확인
    expect(screen.getByTestId('3인')).toBeChecked();
  });
  test('남성을 선택하면 A동과 E동만 선택 가능해야 한다', () => {
    render(<RoommateFilterPage />);

    // 남성 선택
    fireEvent.click(screen.getByLabelText('남성'));
    
    // A동과 E동이 선택 가능한지 확인
    expect(screen.getByLabelText('A동')).not.toBeDisabled();
    expect(screen.getByLabelText('E동')).not.toBeDisabled();

    // B동, C동, D동이 선택 불가능한지 확인
    expect(screen.getByLabelText('B동')).toBeDisabled();
    expect(screen.getByLabelText('C동')).toBeDisabled();
    expect(screen.getByLabelText('D동')).toBeDisabled();
  });
  test('남성을 선택하고 E동을 선택하면 호실 유형 선택이 가능해야 한다', () => {
    render(<RoommateFilterPage />);

    // 남성 선택
    fireEvent.click(screen.getByLabelText('남성'));

    // E동 선택
    fireEvent.click(screen.getByLabelText('E동'));

    // 호실 유형 체크박스가 렌더링되었는지 확인
    expect(screen.getByLabelText('2인실')).toBeInTheDocument();
    expect(screen.getByLabelText('4인실')).toBeInTheDocument();

    // 호실 유형 체크박스가 선택 가능한지 확인
    expect(screen.getByLabelText('2인실')).not.toBeDisabled();
    expect(screen.getByLabelText('4인실')).not.toBeDisabled();
  });
  test('남성을 선택하고 A, E동을 선택했다가 여자를 선택하면 A,E 동의 선택이 해제된다', () => {
    render(<RoommateFilterPage />);

    // 남성 선택
    fireEvent.click(screen.getByLabelText('남성'));

    // A, E동 선택
    fireEvent.click(screen.getByLabelText('E동'));
    fireEvent.click(screen.getByLabelText('A동'));

    // 기숙사 동 체크박스가 체크되었는지 확인
    expect(screen.getByLabelText('E동')).toBeChecked();
    expect(screen.getByLabelText('A동')).toBeChecked();

    expect(screen.getByLabelText('B동')).toBeDisabled();
    expect(screen.getByLabelText('C동')).toBeDisabled();
    expect(screen.getByLabelText('D동')).toBeDisabled();

    //여성 선택
    fireEvent.click(screen.getByLabelText('여성'));

    // 기숙사 동 체크박스가 선택 가능한지 확인
    expect(screen.getByLabelText('E동')).not.toBeChecked();
    expect(screen.getByLabelText('A동')).not.toBeChecked();
    expect(screen.getByLabelText('E동')).toBeDisabled();
    expect(screen.getByLabelText('A동')).toBeDisabled();

    expect(screen.getByLabelText('B동')).not.toBeDisabled();
    expect(screen.getByLabelText('C동')).not.toBeDisabled();
    expect(screen.getByLabelText('D동')).not.toBeDisabled();
  });
});


테스트가 성공적으로 완료된 것을 확인할 수 있다.

profile
안녕하세요. 웹 프론트엔드 개발자 앞잡이 '꼬마돌' 입니다.

0개의 댓글