jest 테스트 도입기...

Jinmin Kim·2025년 7월 15일

매번 개발을 하고, 유지보수를 하며, 계속 테스트를 해가는것들이 있다.
특히 유지보수 측면에서, 문제가있거나, 또다른 기능이 추가되었을때
1. 기존의 기능들이 잘돌아가는지
2. 새로운 기능들이 사이드이팩트는 없는지
등을 고려하며 QA도 테스트를 하겠지만, 나도 테스트를 해보아야하기에
귀찮음을 느끼고있던중, 테스트 자동화 라는 문구와 함께 jest라는 것을 접하게되었다.

이미 여러 FE개발자들이 사용중이며, 우리회사에서 사용중이지 않은 기술에 대해 또
내가 도입할수있는 기회를 얻게될것같아, jest라는 테스트 도구를 공부하며 도입해보고자 한다.

먼저 Jest란? 무엇인가.

Jest란?

React 18 기반 UI 테스트에 최적화된 테스팅 프레임워크로, 페이스북이 만든 Jest는 빠른 실행 속도와 간편한 설정, 풍부한 모킹(Mock) 기능을 제공합니다.

단일 도구로 테스트 러너, 어서션 라이브러리, 커버리지 리포트, 스냅샷(Snapshot) 테스트를 모두 지원합니다.

npm install --save-dev jest @testing-library/react @testing-library/jest-dom babel-jest

테스트 파일 구조

tests/ 또는 .test.jsx / .spec.jsx 형태로 관리
describe → test 혹은 it → expect 패턴을 권장

src/
├── components/
│   └── Button.jsx
└── __tests__/
    └── Button.test.jsx

테스트를 어떻게하는거지??

  • Button.jsx
import React from "react";
import styled from "styled-components";

const StyledButton = styled.button`
  background-color: #0070f3;
  color: white;
  padding: 0.5rem 1rem;
  border: none;
  border-radius: 4px;
`;

export const Button = ({ children, onClick }) => (
  <StyledButton onClick={onClick}>{children}</StyledButton>
);
  • Button.test.js
import React from "react";
import { render, screen, fireEvent } from "@testing-library/react";
import "@testing-library/jest-dom";
import { Button } from "./Button";

describe("🌟 Button 컴포넌트", () => {
  test("✅ 버튼이 렌더링되고 클릭 이벤트가 호출되어야 한다", () => {
    const handleClick = jest.fn();
    render(<Button onClick={handleClick}>클릭!</Button>);
    const btn = screen.getByRole("button", { name: "클릭!" });
    expect(btn).toBeInTheDocument();
    fireEvent.click(btn);
    expect(handleClick).toHaveBeenCalledTimes(1);
  });
});

일단 React와 비슷하면서도 다른거같다.
그래서 기초부터 하나씩 정리해보자.

🏆 1. Jest 사용 기본

🔥 1.1 테스트 구조

🤼‍♂️ describe:

  • 테스트 그룹을 정의합니다. 관련 테스트 케이스들을 하나의 그룹으로 묶어서 관리할 수 있습니다.
  • 예시:
    describe("MyComponent", () => {
      // 여러 개의 it/test 케이스를 그룹화
    });

🤼‍♂️ it / test:

  • 개별 테스트 케이스를 정의합니다. ittest는 기능적으로 동일하며, 하나의 테스트 시나리오를 설명합니다.
  • 예시:
    
    it("should render correctly", () => {
      // 테스트 내용
    });
    

🤼‍♂️ expect:

  • 실제 값과 예상 값을 비교하는 단언(assertion) 함수입니다.
  • 예시:
    
    expect(value).toBe(expectedValue);
    expect(array).toContain(item);

🔥 1.2 설정 및 라이프사이클 훅

🤼‍♂️ beforeEach / afterEach:

  • 각각의 테스트 케이스가 실행되기 전에 또는 후에 특정 작업을 수행할 수 있습니다.
  • 예시:
    beforeEach(() => {
      // 각 테스트 전에 초기화 작업
    });
    
    afterEach(() => {
      jest.clearAllMocks(); // 모든 모의(mock) 함수의 호출 기록 초기화
    });

🤼‍♂️ setupFiles / setupFilesAfterEnv:

  • Jest가 테스트를 실행하기 전에 특정 파일을 로드할 수 있습니다. 예를 들어, 글로벌 polyfill을 설정하는 경우에 사용됩니다.

🏆  2. 위 테스트 코드에서 사용된 Jest 문법 설명

아래는 제공된 테스트 코드의 주요 부분과 그 역할을 설명한 내용입니다.

🤼‍♂️ 2.1 모듈 모킹: jest.mock()

jest.mock("@/services/store/zustand/acnts", () => {
  return jest.fn();
});
  • 역할:
    • 특정 모듈(여기서는 @/services/store/zustand/acnts)을 모킹하여, 테스트 환경에서 해당 모듈의 실제 구현 대신, 모의(mock) 함수나 원하는 값을 반환하도록 합니다.
  • jest.fn():
    • Jest에서 제공하는 모의 함수(mock function) 생성기로, 호출 기록을 추적하고, 원하는 반환값을 설정할 수 있습니다.

🤼‍♂️ 2.2 실제 모듈 가져오기: jest.requireActual()


jest.mock("react-router", () => {
  const actual = jest.requireActual("react-router");
  return {
    ...actual,
    useLocation: jest.fn(),
  };
});
  • 역할:
    • 원래 모듈(여기서는 react-router)의 실제 구현을 가져오면서, 특정 함수(useLocation)만 모킹할 때 사용됩니다.
    • jest.requireActual("react-router")를 통해 다른 기능은 그대로 사용하고, useLocationjest.fn()으로 덮어씌웁니다.

🔥 예시 코드

describe("useMakeGetTradeParams hook", () => {
  beforeEach(() => {
    useLocation.mockReturnValue({ pathname: "/accounts/mygroup/123" });
    acntsStore.mockImplementation((selector) =>
      selector({
        state: {
          tradeParam: { sort_type: "desc" },
          accountsBasic: { selectedUserGroupNo: "abc" },
        },
        handler: {},
      })
    );
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should produce correct trade parameters for "mygroup" menu', async () => {
    render(
      <MemoryRouter>
        <TestMakeGetTradeParams />
      </MemoryRouter>
    );

    await waitFor(() =>
      expect(screen.getByTestId("result").textContent).not.toBe("loading")
    );

    const result = JSON.parse(screen.getByTestId("result").textContent);

    expect(result).toEqual({
      column_kind: "updated_timestamp",
      sort_type: "desc",
      trade_user_bookmark: "N",
      latest_trade: "N",
      trade_type: "C",
      trade_group_no: "123",
    });
  });
});

🤼‍♂️ 상세 설명

🥇describe:

  • 테스트 그룹을 정의합니다. 여기서는 "useMakeGetTradeParams hook"에 대한 테스트 그룹입니다.

🥇beforeEach:

  • 각 테스트가 실행되기 전에 실행됩니다.
  • useLocation.mockReturnValue({ pathname: "/accounts/mygroup/123" });로 특정 pathname을 설정하고, acntsStore를 모킹하여 필요한 상태 객체를 반환하도록 합니다.
  • acntsStore 모킹 함수
    • acntsStore는 원래 Zustand 스토어를 나타내는 함수입니다.
    • 이 함수는 보통 "selector" 함수를 인자로 받아, 스토어의 상태에서 특정 부분을 선택하여 반환합니다.
    • Jest의 mockImplementation을 사용하면, acntsStore가 호출될 때 실행되는 함수를 직접 정의할 수 있습니다.
  • selector 함수 인자
    • 모킹된 acntsStore 함수는 하나의 인자, 즉 selector 함수를 받습니다.
    • selector 함수는 스토어 전체 상태에서 원하는 부분만 추출하도록 설계되어 있습니다.
  • 모킹된 상태 객체
    • selector 함수에 전달되는 객체는 스토어의 가짜 상태(mock state)입니다.
  beforeEach(() => {
//    useLocation.mockReturnValue({ pathname: "/accounts/mygroup/123" });
    acntsStore.mockImplementation((selector) =>
      selector({
        state: {
          tradeParam: { sort_type: <"desc" },
          accountsBasic: { selectedUserGroupNo: "abc" },
        },
        handler: {},
      })
    );
  });

🥇afterEach:

  • 각 테스트가 끝난 후 jest.clearAllMocks()를 호출하여 모의 함수의 호출 기록과 상태를 초기화합니다.

🥇it:

  • 개별 테스트 케이스를 정의합니다. 여기서는 주어진 경로와 모킹된 상태에 대해 makeGetTradeParam이 올바른 객체를 반환하는지 확인합니다.

🥇waitFor:

  • 비동기적으로 DOM 업데이트가 완료될 때까지 기다립니다.
  • "loading"이 아닌 텍스트가 렌더링될 때까지 대기한 후, 결과를 검증합니다.

🥇expect:

  • 최종적으로 화면에 렌더링된 결과가 예상한 객체와 일치하는지 검증합니다.
// Button.test.jsx
//화면에 testid로 button이 있는지 확인한다. 
const buttonElement = screen.getByTestId("button");

//toBeInTheDocument 매처는 해당 요소가 실제로 DOM에 존재하는지를 확인합니다.
expect(buttonElement).toBeInTheDocument();
//버튼 요소가 "test"라는 텍스트를 포함하고 있는지 확인합니다.
expect(buttonElement).toHaveTextContent("test");
//button element에 접근해서 click 이벤트 호출
fireEvent.click(buttonElement);

// Button.jsx
import React from "react";

const ButtonTest = ({ id, label, onClick }) => (
  <button data-testid={id} onClick={onClick}>
    {label}
  </button>
);

export default ButtonTest;

🥇screen

역할:

screen객체는 render() 함수를 통해 렌더링된 결과물을 나타내며,

이 DOM 내의 요소들을 쉽게 찾아내고, 검증할 수 있도록 다양한 쿼리 메서드(getByTestId, getByText, queryByRole 등)를 제공합니다.


    // useEffect에서 값이 업데이트될 때까지 기다림
    await waitFor(() =>
      expect(screen.getByTestId("result").textContent).not.toBe("loading"),
    );
  • screen.getByTestId(testId)
    • 설명:data-testid 속성이 testId와 일치하는 단일 요소를 반환합니다. 요소를 찾지 못하면 에러를 발생시킵니다.
    • 예시:
      
      const element = screen.getByTestId("button");
      
  • screen.getByText(text, options?)
    • 설명:주어진 텍스트를 포함하는 요소를 반환합니다. 텍스트 매칭에 정규식이나 옵션을 사용할 수 있습니다.
    • 예시:
      const element = screen.getByText("Submit");
  • screen.getByRole(role, options?)
    • 설명:ARIA 역할(role)을 기반으로 요소를 찾습니다. 접근성(A11y)을 고려한 테스트에 유용합니다.
    • 예시:
      const button = screen.getByRole("button", { name: /submit/i });
      • screen.findByRole("heading", { level: 2 }, { timeout: 5000 });
        - *비동기적(Asynchronous)**으로 동작하며, 내부적으로 waitFor와 유사한 방식으로 동작합니다.
        - 지정한 timeout(예: { timeout: 5000 })까지 주기적으로 DOM을 쿼리하여 조건에 맞는 요소가 나타날 때까지 기다립니다.

        	screen.findByRole("heading", { level: 2 }, { timeout: 5000 });
  • screen.getByLabelText(labelText, options?)
    • 설명:<label>과 연관된 입력 필드 등, 라벨 텍스트를 기반으로 요소를 찾습니다.
    • 예시:
      const input = screen.getByLabelText("Username");
  • screen.getByPlaceholderText(placeholder, options?)
    • 설명:입력 필드의 placeholder 속성을 기반으로 요소를 찾습니다.
    • 예시:
      
      const input = screen.getByPlaceholderText("Enter your name");
  • screen.getByAltText(altText, options?)
    • 설명:이미지의 alt 속성을 기반으로 요소를 찾습니다.
    • 예시:
      
      const image = screen.getByAltText("User avatar");
  • screen.getByTitle(title, options?)
    • 설명:요소의 title 속성을 기반으로 요소를 찾습니다.
    • 예시:
      const element = screen.getByTitle("Close");

🥇fireEvent

사용 목적:

  • 사용자 이벤트 시뮬레이션:실제 브라우저에서 사용자가 버튼을 클릭하거나, 입력 필드에 텍스트를 입력하는 등의 행동을 테스트 환경에서 흉내 낼 수 있습니다.
  • 이벤트 핸들러 검증:이벤트가 발생했을 때 컴포넌트 내부에서 올바른 함수가 호출되거나 상태가 업데이트되는지 확인할 수 있습니다.
//클릭이벤트
import { fireEvent, render, screen } from "@testing-library/react";
import MyButton from "./MyButton";

test("calls onClick handler when clicked", () => {
  const handleClick = jest.fn();
  render(<MyButton onClick={handleClick} label="Click Me" />);
  const button = screen.getByText("Click Me");

  // 버튼을 클릭하는 이벤트를 시뮬레이션합니다.
  fireEvent.click(button);

  expect(handleClick).toHaveBeenCalledTimes(1);
});

//입력 이벤트
import { fireEvent, render, screen } from "@testing-library/react";
import MyInput from "./MyInput";

test("updates input value on change", () => {
  render(<MyInput />);
  const input = screen.getByPlaceholderText("Enter text");

  // 입력 필드에 "Hello"를 입력하는 이벤트를 시뮬레이션합니다.
  fireEvent.change(input, { target: { value: "Hello" } });

  expect(input.value).toBe("Hello");
});

🥇userEvent

fireEvent보다 좀 더 자연스러운 유저의 이벤트 처리

🥊 폼 제출

import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import MyForm from "./MyForm"; // 테스트 대상 폼 컴포넌트

test("user submits the form", async () => {
  render(<MyForm />);
  const input = screen.getByPlaceholderText("Enter text");
  const submitButton = screen.getByRole("button", { name: /submit/i });

  await userEvent.type(input, "test input");
  await userEvent.click(submitButton);

  // 예: 폼 제출 후 결과 메시지가 나타난다고 가정
  expect(await screen.findByText("Form submitted")).toBeInTheDocument();
});

🔥 모듈을 모킹하는 이유는:

  • 테스트 환경을 완전히 제어하고 외부 요인으로부터 격리하기 위해서입니다.

예시)

  • useLocation을 모킹함으로써, 테스트할 때 항상 동일한 경로를 제공하여 훅의 동작을 예측 가능하게 만듭니다.
  • acntsStore를 모킹하면, 스토어의 상태를 고정하여 테스트의 일관성을 유지할 수 있습니다.
profile
Let's do it developer

0개의 댓글