매번 개발을 하고, 유지보수를 하며, 계속 테스트를 해가는것들이 있다.
특히 유지보수 측면에서, 문제가있거나, 또다른 기능이 추가되었을때
1. 기존의 기능들이 잘돌아가는지
2. 새로운 기능들이 사이드이팩트는 없는지
등을 고려하며 QA도 테스트를 하겠지만, 나도 테스트를 해보아야하기에
귀찮음을 느끼고있던중, 테스트 자동화 라는 문구와 함께 jest라는 것을 접하게되었다.
이미 여러 FE개발자들이 사용중이며, 우리회사에서 사용중이지 않은 기술에 대해 또
내가 도입할수있는 기회를 얻게될것같아, 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
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>
);
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와 비슷하면서도 다른거같다.
그래서 기초부터 하나씩 정리해보자.
describe("MyComponent", () => {
// 여러 개의 it/test 케이스를 그룹화
});it과 test는 기능적으로 동일하며, 하나의 테스트 시나리오를 설명합니다.
it("should render correctly", () => {
// 테스트 내용
});
expect(value).toBe(expectedValue);
expect(array).toContain(item);beforeEach(() => {
// 각 테스트 전에 초기화 작업
});
afterEach(() => {
jest.clearAllMocks(); // 모든 모의(mock) 함수의 호출 기록 초기화
});아래는 제공된 테스트 코드의 주요 부분과 그 역할을 설명한 내용입니다.
jest.mock()jest.mock("@/services/store/zustand/acnts", () => {
return jest.fn();
});
@/services/store/zustand/acnts)을 모킹하여, 테스트 환경에서 해당 모듈의 실제 구현 대신, 모의(mock) 함수나 원하는 값을 반환하도록 합니다.jest.fn():jest.requireActual()
jest.mock("react-router", () => {
const actual = jest.requireActual("react-router");
return {
...actual,
useLocation: jest.fn(),
};
});
react-router)의 실제 구현을 가져오면서, 특정 함수(useLocation)만 모킹할 때 사용됩니다.jest.requireActual("react-router")를 통해 다른 기능은 그대로 사용하고, useLocation만 jest.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",
});
});
});
useLocation.mockReturnValue({ pathname: "/accounts/mygroup/123" });로 특정 pathname을 설정하고, acntsStore를 모킹하여 필요한 상태 객체를 반환하도록 합니다.acntsStore는 원래 Zustand 스토어를 나타내는 함수입니다.mockImplementation을 사용하면, acntsStore가 호출될 때 실행되는 함수를 직접 정의할 수 있습니다.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: {},
})
);
});
jest.clearAllMocks()를 호출하여 모의 함수의 호출 기록과 상태를 초기화합니다.makeGetTradeParam이 올바른 객체를 반환하는지 확인합니다.// 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객체는 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?)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");사용 목적:
//클릭이벤트
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");
});
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를 모킹하면, 스토어의 상태를 고정하여 테스트의 일관성을 유지할 수 있습니다.