본문 바로가기

programming/React

Next.js with Typescript 프로젝트 테스트 코드 작성하기(jest, testing-library/react)

1. 현재 프로젝트 구성

Next.js App router 사용

Typescript

Eslint 

 

2. 테스트 툴 선택

Jest 와 Testing Library 를 사용하기로 결정

 

3. 세팅

1) Testing Libray React 패키지 설치

(참고 : Installation - With Typescript)

npm install --save-dev @testing-library/react @testing-library/dom @types/react @types/react-dom

 

2) Jest (React) 패키지 설치

(참고: Testing React Apps - Setup With Create React App)

npm install --save-dev jest react-test-renderer

 

(참고: Next.js JEST- Manual Setup)

npm install -D jest-environment-jsdom @testing-library/jest-dom

 

3) Typescript 용 추가 패키지 설치

npm i --save-dev @types/jest ts-jest

 

4) Eslint 용 추가 패키지 설치

npm i -D eslint-plugin-jest

 

 

5) 테스트 스크립트 추가

✅ 설치 후 package.json

"scripts": {
    "test": "jest"
},

"dependencies": {
    "react": "^18",
    "react-dom": "^18",
},

"devDependencies": {
    "@testing-library/dom": "^10.1.0",
    "@testing-library/jest-dom": "^6.4.6",
    "@testing-library/react": "^16.0.0",

    "@types/jest": "^29.5.12",
    "@types/react": "^18.3.3",
    "@types/react-dom": "^18.3.0",

    "jest": "^29.7.0",
    "jest-environment-jsdom": "^29.7.0",
    "react-test-renderer": "^18.3.1",

    "eslint-plugin-jest": "^28.6.0",

    "ts-jest": "^29.1.5",
}

 

6) tsconfig.json 에 types 추가

  "compilerOptions": {
    "types": ["node", "jest", "@testing-library/jest-dom"],

 

7) jest init 으로 기본 config 세팅

npm init jest@latest


✅ jset.config.ts 파일 확인

Next.js Jest config 사용하여 수정 (참고

/**
 * For a detailed explanation regarding each configuration property, visit:
 * https://jestjs.io/docs/configuration
 */

import type { Config } from "jest";
import nextJest from "next/jest.js";

const createJestConfig = nextJest({
  // Provide the path to your Next.js app to load next.config.js and .env files in your test environment
  dir: "./",
});

const config: Config = {
  // Automatically clear mock calls, instances, contexts and results before every test
  clearMocks: true,
 
  // An array of file extensions your modules use
  moduleFileExtensions: ["js", "mjs", "cjs", "jsx", "ts", "tsx", "json", "node"],

  // A preset that is used as a base for Jest's configuration
  preset: "ts-jest",

  // The test environment that will be used for testing
  testEnvironment: "jsdom",
};

export default createJestConfig(config);

 

8) .eslintrc.json에 설정 추가 (기존설정에 jest 관련 설정 추가)

"plugins": ["jsx-a11y", "@typescript-eslint", "prettier", "jest"],

"env": {
    // 전역객체를 eslint가 인식하는 구간
    "browser": true, // document나 window 인식되게 함
    "node": true,
    "es6": true,
    "jest/globals": true
},

"ignorePatterns": [
    "node_modules/",
    ".eslintrc.js",
    "next.config.js",
    "postcss.config.js",
    "run_server.js",
    "jest.config.ts"
], // eslint 미적용될 폴더나 파일 명시

 

 

4. 예제 코드 작성

(참고 - Jest Dom Testing)

 

Jest 페이지에서 제공하는 CheckboxWithLabel.js 컴포넌트를 만들고, 테스트 코드를 작성

typescript 버전으로 작성

 

1) CheckboxWithLabel.tsx

import { useState } from "react";

export default function CheckboxWithLabel({ labelOn, labelOff }: { labelOn: string; labelOff: string }) {
  const [isChecked, setIsChecked] = useState(false);

  const onChange = () => {
    setIsChecked(!isChecked);
  };

  return (
    <label>
      <input type="checkbox" checked={isChecked} onChange={onChange} />
      {isChecked ? labelOn : labelOff}
    </label>
  );
}

 

 

2) 프로젝트 루트에 __tests__/CheckboxWithLabel.test.tsx

import { cleanup, fireEvent, render } from "@testing-library/react";

import CheckboxWithLabel from "@/components/ui/CheckboxWithLabel";

// Note: running cleanup afterEach is done automatically for you in @testing-library/react@9.0.0 or higher
// unmount and cleanup DOM after the test is finished.
afterEach(cleanup);

it("CheckboxWithLabel changes the text after click", () => {
  const { queryByLabelText, getByLabelText } = render(<CheckboxWithLabel labelOn="On" labelOff="Off" />);

  expect(queryByLabelText(/off/i)).toBeTruthy();

  fireEvent.click(getByLabelText(/off/i));

  expect(queryByLabelText(/on/i)).toBeTruthy();
});

 

3) 테스트 실행

npm run test

 

 

5. 실제 내가 원하는 테스트 내용

  • url 로 다국가 처리중 : /{country}/{language} 👉 /kr/ko, /us/en, /ch/zh, /tw/zh, /jp/ja... 
  • 국가별로 지원하는 서비스가 다름 
    • kr : 주소로 배달, 매장 픽업
    • us : 주소로 배달, 매장 픽업
    • tw : 주소로 배달, 매장 픽업, 편의점 픽업
    • jp : 주소로 배달
  • next-intl 을 사용하여 다국어 처리를 하고 있음
    • /locales/ko/common.json 파일 안에 key-value 값으로 관리
  • <ShippingOptions> 라는 컴포넌트는 국가에 맞게, 필요한 버튼을 노출시켜주는 컴포넌트

TODO 👉 테스트 내용 : url 로 국가가 변경될 때마다 적절한 버튼들이 랜더링 되는가

 

1) ShippingOptions 컴포넌트 코드

import { useParams } from "next/navigation";
import { useEffect } from "react";

import { useCheckoutStore } from "@/shared/libs/stores/checkout";

import ConveniencePickup from "./_function/ConveniencePickup";
import ShipToAddress from "./_function/ShipToAddress";
import StorePickup from "./_function/StorePickup";

export default function ShippingOptions() {
  const { country } = useParams();

  const { updateShippingOption } = useCheckoutStore();

  useEffect(() => {
    if (!["kr", "us", "tw"].includes(country as string)) {
      updateShippingOption("shipToAddress");
    }
  }, [country]);

  if (["kr", "us"].includes(country as string))
    // 스토어 픽업 지원 국가
    return (
      <div className="flex gap-[8px] mb-[24px]">
        <ShipToAddress />
        <StorePickup />
      </div>
    );

  if (country === "tw")
    // 스토어 픽업 & 편의점 픽업 지원 국가
    return (
      <div className="flex gap-[8px] flex-col">
        <ShipToAddress />
        <StorePickup />
        <ConveniencePickup />
      </div>
    );
  return (
    <div className="mb-[24px]">
      <ShipToAddress />
    </div>
  );
}

 

2) /__tests__/ShippingOptions.test 테스트 코드

/* eslint-disable import/no-dynamic-require */
/* eslint-disable global-require */
import "@testing-library/jest-dom";
import { render } from "@testing-library/react";
import { useParams } from "next/navigation";
import { NextIntlProvider } from "next-intl";

import ShippingOptions from "@/components/pages/Checkout/ShippingStep/ShippingOptions";

// Mock useParams from next/navigation
jest.mock("next/navigation", () => ({
  useParams: jest.fn(),
}));

describe("ShippingOptions", () => {
  const useRouter = jest.spyOn(require("next/router"), "useRouter");
  const locale = "ko";
  const messages = require(`../locales/${locale}/common.json`);

  useRouter.mockImplementationOnce(() => ({
    query: { locale },
  }));

  it("kr 은 2가지 shipping option 을 리턴한다 (배송지 주소 / 매장에서 픽업)", () => {
    (useParams as jest.Mock).mockReturnValue({ country: "kr" });
    const { debug } = render(
      <NextIntlProvider messages={messages} locale={locale}>
        <ShippingOptions />
      </NextIntlProvider>,
    );

    // expect(getByText("배송지 주소")).toBeInTheDocument();
    // expect(getByText("매장에서 픽업")).toBeInTheDocument();
    debug();
  });

  it("jp 은 1가지 shipping option 을 리턴한다 (배송지 주소)", () => {
    (useParams as jest.Mock).mockReturnValue({ country: "jp" });
    const { getByText } = render(
      <NextIntlProvider messages={messages} locale={locale}>
        <ShippingOptions />
      </NextIntlProvider>,
    );

    expect(getByText("배송지 주소")).toBeInTheDocument();
  });

  it("tw 은 3가지 shipping option 을 리턴한다 (배송지 주소 / 매장에서 픽업 / 편의점에서 픽업)", () => {
    (useParams as jest.Mock).mockReturnValue({ country: "tw" });
    const { getByText } = render(
      <NextIntlProvider messages={messages} locale={locale}>
        <ShippingOptions />
      </NextIntlProvider>,
    );

    expect(getByText("배송지 주소")).toBeInTheDocument();
    expect(getByText("매장에서 픽업")).toBeInTheDocument();
    expect(getByText("편의점에서 픽업")).toBeInTheDocument();
  });
});

 

3) 테스트 실행

npm run test

 

4) 테스트 결과

debug() 를 사용하면, 실제 생성되는 html 을 확인 할 수 있다