Test

[Jest] React + Vite + TypeScript 프로젝트에 Jest 테스트 환경 구축

jalalja 2024. 12. 16. 23:45

BDD (Behavior-Driven Development) 란?

더보기

- BDD 란?

  • 구현에 중점을 두기보다는 동작에 중점을 두는 테스트 개발 방식이다.
  • 사용자가 원하는 결과를 정확히 표현하는것이 목적이다.
  • 테스트는 개발자와 비즈니스 이해 관계자 모두가 읽을 수 있게 작성한다.
  • React 테스트는 주로 BDD 방식으로 진행한다.

React + Vite + TypeScript 프로젝트 생성

더보기

- Vite 프로젝트 생성

yarn create vite

 

- 프로젝트 이름 입력

 

- React 선택

 

- TypeScript + SWC 선택

  • SWC (Speedy Web Compiler) : JavaScript 코드 중 ES6 이상으로 작성된 최신 문법을 ES5로 트랜스파일 하거나, TypeScript 코드를 JavaScript 코드로 컴파일하는데 사용되는 빌드 도구이다.

 

- node_modules 생성

yarn install

Vitest가 아닌 Jest를 선택한 이유

더보기

- Jest를 선택한 이유

  • 프로젝트가 Vite로 생성되었지만, Jest를 선택한 이유는 Vite와의 호환성뿐만 아니라 범용성을 고려했기 때문이다.
  • Vitest에 비해 Jest는 더 널리 사용되고 다양한 커뮤니티와 도구 지원을 갖춘 테스트 프레임워크라는 인식도 존재했다.
  • 또한, 추후 마이그레이션을 고려할 때 Jest는 다양한 환경과의 호환성이 뛰어나고, 널리 채택된 표준이기 때문에 보다 유연하고 지속 가능한 선택이라고 판단했다.
  • 이러한 이유로 Vite의 이점을 살리면서도, 향후 다른 환경으로의 전환에 대비해 Jest를 선택하는 것이 더 합리적이라고 생각했다.

 

- Jest 공식 홈페이지에서도 Vite 환경에서 Jest를 사용하기에는 지원하지 않는 부분이 존재한다고 하였다.

 

- 참고

 

vite-jest/packages/vite-jest at main · haoqunjiang/vite-jest

First-class Vite integration for Jest. Contribute to haoqunjiang/vite-jest development by creating an account on GitHub.

github.com


Jest 설치 (실제 프로젝트에 적용한 케이스)

더보기

- jest, ts-jest, @types/jest , @jest/globals 설치

  • jest : Jest를 사용하기 위한 핵심 라이브러리로 테스트 실행, 매칭 함수 등을 제공한다.
  • ts-jest : TypeScript로 작성된 프로젝트를 Jest로 테스트할 수 있게 해주는 Jest 변환기이다.
  • @types/jest : Jest에 대한 타입 정의를 위한 모듈이다.
  • @jest/globals : Jest의 테스트 함수들을 import 하여 명시적으로 사용하는 코드를 확인할 수 있다.
yarn add -D jest ts-jest @types/jest @jest/globals

 

- jest 설정 파일 생성

yarn ts-jest config:init

 

- jest.config.js 설정 파일 변경

  • testEnvironment: "jsdom"
    • Jest 테스트를 어떤 가상 환경에서 실행할지에 대한 설정이다.
    • node : Node.js 환경에서 테스트를 실행한다.
    • jsdom : DOM API를 포함하는 브라우저와 유사한 환경에서 테스트를 실행한다.
  • tsconfig : "tsconfig.app.json"
    • tsconfig.app.json 파일을 인식 못하는 문제가 가끔 생겨서 명시적으로 작성해준다.
    • tsconfig.node.json : 노드에서 사용하는 설정 파일이다.
    • tsconfig.app.json : 브라우저에서 사용하는 설정 파일이다.
  • setupFiles: ["./jestSetup.ts"]
    • jestSetup.ts : Jest가 테스트를 실행하기 전에 특정 작업을 수행할 수 있도록 도와주는 역할을 한다. (Jest의 BeforAll 함수와 동일한 역할 제공)
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
  testEnvironment: "jsdom",
  transform: {
    "^.+.tsx?$": [
      "ts-jest",
      {
        tsconfig: "tsconfig.app.json",
      },
    ],
  },
  setupFiles: ["./jestSetup.ts"],
};

 

- jestSetup.ts 설정 파일 변경

  • react에서 제공하는 act() 함수를 동작시키기 위해서는 필수적으로 설정해주어야 한다.
  • act()
    • act()는 인자로 받은 함수를 실행시켜 가상의 DOM(jsdom)에 적용하는 역할을 한다.
    • act()는 테스트 중 React 컴포넌트의 상태 변화가 DOM에 완전히 반영되었는지를 보장하기 때문에 React가 브라우저에서 실행될 때와 최대한 비슷한 환경에서 테스트할 수 있다.
    • 추후에 testing-library/react를 사용해 보다 편하게 act()를 사용할 수 있다.
(global as any).IS_REACT_ACT_ENVIRONMENT = true;

 

- jestSetup.ts 설정 파일에서 as any 사용시 error가 발생하는 상황

  • 원인
    • TypeScript와 ESLint를 함께 사용할 때 발생하는 문제로, 코드에서 any 타입을 사용했기 때문에 나타난다.
    • ESLint의 @typescript-eslint/no-explicit-any 규칙은 명시적으로 any 타입을 사용하는 것을 방지하기 때문이다.
  • 해결
    • eslint.config.js 파일의 rules 부분에 해당 설정을 off로 지정한다.
    rules: {
      ...reactHooks.configs.recommended.rules,
      "react-refresh/only-export-components": [
        "warn",
        { allowConstantExport: true },
      ],
      "@typescript-eslint/no-explicit-any": "off", // 추가
    },

 

- 전체 폴더 구조


Jest 설치 (기록용)

더보기

- jest, ts-jest, @types/jest, @jest/globals 설치

  • jest : Jest를 사용하기 위한 핵심 라이브러리로 테스트 실행, 매칭 함수 등을 제공한다.
  • ts-jest : TypeScript로 작성된 프로젝트를 Jest로 테스트할 수 있게 해주는 Jest 변환기이다.
  • @types/jest : Jest에 대한 타입 정의를 위한 모듈이다.
  • @jest/globals : Jest의 테스트 함수들을 import 하여 명시적으로 사용하는 코드를 확인할 수 있다.
yarn add -D jest ts-jest @types/jest @jest/globals

 

- jest 설정 파일 생성

yarn jest --init
  • Would you like to use Jest when running "test" script in "package.json"? => yes
    • package.json의 script에 "test": "jest" 코드를 추가할지 묻는 질문이다.
    • 해당 스크립트를 통해 yarn test를 사용해서 jest를 실행할 수 있다. (yarn jest로도 실행이 가능하기때문에 no로 설정해도 무방하다)
  • Would you like to use Typescript for the configuration file? => yes
    • Jest 설정 파일을 TypeScript(jest.config.ts)로 생성할지 묻는 질문이다.
    • 프로젝트가 TypeScript를 사용한다면 yes로 설정해주는 것이 좋다. (no를 선택할 경우 js 파일로 생성된다)
  • Choose the test environment that will be used for testing => js-dom (browser-like)
    • Jest 테스트를 어떤 가상 환경에서 실행할지를 묻는 질문이다.
    • node : Node.js 환경에서 테스트를 실행한다. (Node.js 기반 프로젝트에 적합하다)
    • jsdom : DOM API를 포함하는 브라우저와 유사한 환경에서 테스트를 실행한다. (프론트엔드 프로젝트에 적합하다)
  • Do you want Jest to add coverage reports? => yes
    • 테스트 실행 시 코드 커버리지 리포트를 생성할지 묻는 질문이다. (프로젝트의 품질을 보다 높이고 싶다면 사용하는것이 좋다)
  • Which provider should be used to instrument code for coverage? => v8
    • Jest에서 코드 커버리지를 측정하기 위해 어떤 도구를 사용할지 선택하는 질문이다.
    • v8을 선택한 이유 : v8이 babel보다 빠르고 효율적이고, vite 프로젝트는 babel을 사용하지 않고 ESBuild를 사용하기 때문이다.
  • Automatically clear mock calls, instances, contexts and results before every test? => yes
    • Jest 테스트 프레임워크에서 mock 함수의 상태를 자동으로 초기화할지 여부를 묻는 질문이다.
    • 테스트간의 독립성을 보장하기 위해서 사용하는것이 좋다. (사용을 하지 않을 경우에는 jest.clearAllMocks() 함수를 사용해 직접 초기화해줘야 한다)

기본 테스트 (React Testing Library 사용하지 않은 경우)

더보기

- App.tsx 코드

  • 기본적인 로그인 화면을 보여준다. (디자인 X)
import { FormEvent, useState } from "react";
import "./App.css";

function App() {
  const [email, setEmail] = useState<string>("");
  const [password, setPassword] = useState<string>("");
  const [loginError, setLoginError] = useState<boolean>(false);

  const onSubmit = (e: FormEvent) => {
    e.preventDefault();

    if (!email || !password) {
      setLoginError(true);
      return;
    }
  };

  return (
    <>
      <header>
        <h1>HEADER</h1>
      </header>

      {loginError && <p id="errorMessage">ERROR</p>}

      <main>
        <form onSubmit={onSubmit}>
          <div>
            <h2>email</h2>
            <input
              type="text"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
            />
          </div>
          <div>
            <h2>password</h2>
            <input
              type="password"
              value={password}
              onChange={(e) => setPassword(e.target.value)}
            />
          </div>
          <button id="submitButton" type="submit">
            Form 제출
          </button>
        </form>
      </main>

      <footer>
        <h1>FOOTER</h1>
      </footer>
    </>
  );
}

export default App;

 

- 렌더링 테스트 (App.test.tsx)

  • header 태그 안에 있는 h1 태그의 텍스트가 HEADER인지 확인하는 테스트를 수행한다. (렌더링이 잘 되는지 확인하기위한 용도)
import { test } from "@jest/globals";
import { act } from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

test("로그인 화면이 잘 보인다.", async () => {
  const container = document.createElement("div");

  await act(() => {
    ReactDOM.createRoot(container).render(<App />);
  });

  expect(container.querySelector("header h1")?.textContent).toBe("HEADER");
});

 

- 테스트 실행 시 모듈 관련 에러가 발생하는 상황

  • 원인 
    • jest-environment-jsdom 모듈이 설치가 되어있지 않아서 생기는 문제로 보인다.
  • 해결
    • 해당 모듈을 설치해준다.
yarn add -D jest-environment-jsdom

 

- 테스트 실행 시 CSS 파일 관련 에러가 발생하는 상황

  • 원인
    • jest가 CSS 파일을 처리할 수 없어서 발생한 문제다.
  • 해결 1.
    • App.tsx에서 import한 App.css 파일을 제거하거나, App.css파일 내부에서 초기 생성된 모든 클래스를 제거하면 에러가 해결된다.
  • 해결 2.
    • jest는 기본적으로 JavaScript 외에는 처리를 하려고하지 않는다.
    • JavaScript가 아닌 파일(CSS 등)을 가져올 때 오류를 방지할 수 있는 jest-transform-stub 모듈을 받는다.
    • 그리고 jest.config.js 파일에 "^.+\\.css$": "jest-transform-stub" 설정을 추가해준다.
yarn add -D jest-transform-stub
/** @type {import('ts-jest').JestConfigWithTsJest} **/
export default {
  testEnvironment: "jsdom",
  transform: {
    "^.+.tsx?$": [
      "ts-jest",
      {
        tsconfig: "tsconfig.app.json",
      },
    ],
    "^.+\\.css$": "jest-transform-stub", // 추가
  },
  setupFiles: ["./jestSetup.ts"],
};

 

- 테스트 결과 (성공)

 

- 인터렉션 테스트 (App.test.tsx)

  • form 제출 버튼 클릭 시 email, password 중 입력이 되지 않은 부분이 있을 경우 에러 메시지가 보여지는지 확인하는 테스트를 수행한다.
import { test } from "@jest/globals";
import { act } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";

test("이메일, 비밀번호 미입력시 에러메시지가 나타난다.", async () => {
  // 테스트를 위한 #root와 동일한 요소 생성
  const container = document.createElement("div");
  
  // 해당 요소를 body 하단에 삽입
  document.body.appendChild(container);

  // container 안에 App 컴포넌트 렌더링
  await act(() => {
    createRoot(container).render(<App />);
  });

  // form을 제출할 버튼 가져오기
  const submitButton = container.querySelector("#submitButton");

  // 에러 메시지 가져오기
  let errorMessage = container.querySelector("#errorMessage");

  // form을 제출하기 전에는 에러 메시지가 없기때문에 undefined여야 한다.
  expect(errorMessage?.textContent).toBeUndefined();

  // form 제출 버튼 클릭 이벤트
  await act(() => {
    submitButton?.dispatchEvent(new MouseEvent("click"));
  });

  // form을 제출한 다음에 변한 에러 메시지를 가져온다.
  errorMessage = container.querySelector("#errorMessage");

  // 에러 메시지는 ERROR여야 한다.
  expect(errorMessage?.textContent).toBe("ERROR");
});

 

- 테스트 결과 (성공)


React Testing Library 사용