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은 ECMAScript의 공식 모듈 시스템이다. import와 export 구문을 사용하여 모듈을 불러오고 내보낸다. 주로 브라우저 환경에서 사용되며, 최신 Node.js 버전에서도 지원된다.
CommonJS는 서버 사이드 및 데스크톰 애플리케이션을 위해 설계된 모듈 시스템이다. require와 module.exports를 사용하여 모듈을 불러오고 내보낸다. 주로 Node.js 환경에서 사용된다.
Vitest는 JavaScript 및 TypeScript 프로젝트를 위한 모던 테스트 러너 및 테스트 프레임워크이다. Jest처럼 테스트 실행뿐만 아니라 모킹(mocking)과 스냅샷(snapshot)을 지원하며, Jest와 호환되는 API를 제공하고 있어서 Jest 사용자들이 쉽게 전환할 수 있도록 설계되었다.
npm install -D vitest
npm install @vitejs/plugin-react
이는 Vite와 React를 함께 사용할때 필요한 플러그인이다. React 프로젝트에 Vite를 통합할 때 필요한 설정과 최적화를 제공한다.
https://www.npmjs.com/package/@vitejs/plugin-react
"scripts": {
"start": "craco start",
"build": "craco build",
"test": "vitest",
"test:run": "vitest run",
"eject": "react-scripts eject"
},
test 부분을 "vitest"로 변경해준다.
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를 사용할 수 있도록 하였다.
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();
});
});

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