Jest ํ ์คํธ ํ๋ ์์ํฌ ์์ ๊ฐ์ด๋
Jest๋ ๋ฌด์์ธ๊ฐ
Jest๋ Facebook์์ ๊ฐ๋ฐํ JavaScript/TypeScript ํ
์คํธ ํ๋ ์์ํฌ์
๋๋ค.
์ ๋ก ์ค์ ์ผ๋ก ์์ํ ์ ์์ผ๋ฉฐ, ๋ด์ฅ๋ assertion, mocking, ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ๊ธฐ๋ฅ์ ์ ๊ณตํฉ๋๋ค.
์ Jest๋ฅผ ์ฌ์ฉํ ๊น
- ์ ๋ก ์ค์ : ์ถ๊ฐ ์ค์ ์์ด ๋ฐ๋ก ์ฌ์ฉ ๊ฐ๋ฅ
- ๋น ๋ฅธ ์คํ: ๋ณ๋ ฌ ํ ์คํธ ์คํ์ผ๋ก ์๋ ์ต์ ํ
- ๊ฐ๋ ฅํ Mocking: ํจ์, ๋ชจ๋, ํ์ด๋จธ ๋ฑ์ ์ฝ๊ฒ ๋ชจํน
- ์ค๋ ์ท ํ ์คํธ: UI ์ปดํฌ๋ํธ ๋ ๋๋ง ๊ฒฐ๊ณผ ๋น๊ต
- ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง: ํ ์คํธ ์ปค๋ฒ๋ฆฌ์ง ์๋ ์ธก์
์ค์น ๋ฐ ๊ธฐ๋ณธ ์ค์
# npm
npm install --save-dev jest @types/jest
# yarn
yarn add -D jest @types/jest
# TypeScript ์ฌ์ฉ ์
npm install --save-dev ts-jest @types/node
package.json ์ค์ :
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
jest.config.js (๊ธฐ๋ณธ ์ค์ ):
module.exports = {
testEnvironment: "node", // ๋๋ 'jsdom' (๋ธ๋ผ์ฐ์ ํ๊ฒฝ)
roots: ["<rootDir>/src"],
testMatch: ["**/__tests__/**/*.ts", "**/?(*.)+(spec|test).ts"],
transform: {
"^.+\\.ts$": "ts-jest",
},
collectCoverageFrom: ["src/**/*.{ts,tsx}", "!src/**/*.d.ts"],
};
๊ธฐ๋ณธ ๋ฌธ๋ฒ
describe์ it/test
describe("๊ณ์ฐ๊ธฐ ํจ์", () => {
it("๋ ์๋ฅผ ๋ํด์ผ ํจ", () => {
expect(add(1, 2)).toBe(3);
});
test("๋ ์๋ฅผ ๋นผ์ผ ํจ", () => {
expect(subtract(5, 2)).toBe(3);
});
});
Assertion (๋จ์ธ)
// ๋๋ฑ์ฑ
expect(2 + 2).toBe(4); // === ๋น๊ต
expect({ name: "John" }).toEqual({ name: "John" }); // ๊น์ ๋น๊ต
// ์ฐธ/๊ฑฐ์ง
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// ์ซ์
expect(2 + 2).toBeGreaterThan(3);
expect(2 + 2).toBeGreaterThanOrEqual(4);
expect(2 + 2).toBeLessThan(5);
// ๋ฌธ์์ด
expect("team").toMatch(/ea/);
expect("team").toContain("ea");
// ๋ฐฐ์ด
expect(["apple", "banana"]).toContain("apple");
expect([1, 2, 3]).toHaveLength(3);
// ๊ฐ์ฒด
expect({ name: "John", age: 30 }).toHaveProperty("name");
expect({ name: "John" }).toMatchObject({ name: "John" });
// ์์ธ
expect(() => {
throw new Error("์๋ฌ ๋ฐ์");
}).toThrow("์๋ฌ ๋ฐ์");
๋น๋๊ธฐ ํ ์คํธ
// Promise
test("๋น๋๊ธฐ ๋ฐ์ดํฐ ๊ฐ์ ธ์ค๊ธฐ", async () => {
const data = await fetchData();
expect(data).toBeDefined();
});
// ์ฝ๋ฐฑ
test("์ฝ๋ฐฑ ํ
์คํธ", (done) => {
fetchData((data) => {
expect(data).toBeDefined();
done();
});
});
// resolves/rejects
test("Promise ์ฑ๊ณต", async () => {
await expect(fetchData()).resolves.toBe("success");
});
test("Promise ์คํจ", async () => {
await expect(fetchData()).rejects.toThrow("์๋ฌ");
});
Mocking (๋ชจํน)
ํจ์ ๋ชจํน
// jest.fn() - ํจ์ ๋ชจํน
const mockFn = jest.fn();
mockFn(1, 2);
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledWith(1, 2);
expect(mockFn).toHaveReturnedWith(undefined);
// ๋ฐํ๊ฐ ์ค์
const mockFn = jest.fn(() => "mocked value");
expect(mockFn()).toBe("mocked value");
// ์ฌ๋ฌ ๋ฐํ๊ฐ
const mockFn = jest
.fn()
.mockReturnValueOnce("first")
.mockReturnValueOnce("second")
.mockReturnValue("default");
๋ชจ๋ ๋ชจํน
// ๋ชจ๋ ์ ์ฒด ๋ชจํน
jest.mock("./api");
// ๋ถ๋ถ ๋ชจํน
jest.mock("./api", () => ({
...jest.requireActual("./api"),
fetchUser: jest.fn(),
}));
// ๊ตฌํ ์ปค์คํฐ๋ง์ด์ง
jest.mock("./api", () => ({
fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: "John" })),
}));
ํ์ด๋จธ ๋ชจํน
// ํ์ด๋จธ ๋ชจํน
jest.useFakeTimers();
test("1์ด ํ ์คํ", () => {
const callback = jest.fn();
setTimeout(callback, 1000);
jest.advanceTimersByTime(1000);
expect(callback).toHaveBeenCalled();
});
// ์๋ ํ์ด๋จธ๋ก ๋ณต์
jest.useRealTimers();
React ์ปดํฌ๋ํธ ํ ์คํธ
์ค์น:
npm install --save-dev @testing-library/react @testing-library/jest-dom
์ค์ (jest.config.js):
module.exports = {
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
};
jest.setup.js:
import "@testing-library/jest-dom";
์ปดํฌ๋ํธ ํ ์คํธ ์์:
import { render, screen, fireEvent } from "@testing-library/react";
import Button from "./Button";
describe("Button ์ปดํฌ๋ํธ", () => {
it("๋ฒํผ์ด ๋ ๋๋ง๋์ด์ผ ํจ", () => {
render(<Button>ํด๋ฆญ</Button>);
expect(screen.getByText("ํด๋ฆญ")).toBeInTheDocument();
});
it("ํด๋ฆญ ์ด๋ฒคํธ๊ฐ ๋ฐ์ํด์ผ ํจ", () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>ํด๋ฆญ</Button>);
fireEvent.click(screen.getByText("ํด๋ฆญ"));
expect(handleClick).toHaveBeenCalledTimes(1);
});
});
์ค๋ ์ท ํ ์คํธ
import { render } from "@testing-library/react";
import Component from "./Component";
test("์ปดํฌ๋ํธ ์ค๋
์ท", () => {
const { container } = render(<Component />);
expect(container).toMatchSnapshot();
});
์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง
# ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ ์์ฑ
npm run test:coverage
# ํน์ ์๊ณ๊ฐ ์ค์
jest --coverage --coverageThreshold='{
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}'
Next.js์ Jest ํตํฉ
jest.config.js:
const nextJest = require("next/jest");
const createJestConfig = nextJest({
dir: "./",
});
const customJestConfig = {
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
testEnvironment: "jest-environment-jsdom",
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1",
},
};
module.exports = createJestConfig(customJestConfig);
์ค์ ์์
์ ํธ๋ฆฌํฐ ํจ์ ํ ์คํธ
// utils/formatDate.ts
export function formatDate(date: Date): string {
return date.toLocaleDateString("ko-KR");
}
// utils/formatDate.test.ts
import { formatDate } from "./formatDate";
describe("formatDate", () => {
it("๋ ์ง๋ฅผ ํ๊ตญ์ด ํ์์ผ๋ก ํฌ๋งทํด์ผ ํจ", () => {
const date = new Date("2025-04-30");
expect(formatDate(date)).toMatch(/2025/);
});
});
API ํจ์ ํ ์คํธ
// api/user.ts
export async function fetchUser(id: number) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// api/user.test.ts
import { fetchUser } from "./user";
jest.mock("node-fetch", () => jest.fn());
describe("fetchUser", () => {
it("์ฌ์ฉ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์์ผ ํจ", async () => {
const mockUser = { id: 1, name: "John" };
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(mockUser),
})
) as jest.Mock;
const user = await fetchUser(1);
expect(user).toEqual(mockUser);
expect(global.fetch).toHaveBeenCalledWith("/api/users/1");
});
});
๋ฒ ์คํธ ํ๋ํฐ์ค
- ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ผ๋ก ์์ฑ: ๊ฐ ํ ์คํธ๋ ๋ค๋ฅธ ํ ์คํธ์ ์์กดํ์ง ์์์ผ ํจ
- ๋ช
ํํ ํ
์คํธ ์ด๋ฆ:
it('should ...')ํ์์ผ๋ก ์๋๋ฅผ ๋ช ํํ - AAA ํจํด: Arrange(์ค๋น) โ Act(์คํ) โ Assert(๊ฒ์ฆ)
- ํ ๋ฒ์ ํ๋๋ง ํ ์คํธ: ํ๋์ ํ ์คํธ๋ ํ๋์ ๋์๋ง ๊ฒ์ฆ
- Mock์ ์ต์ํ: ํ์ํ ๊ฒฝ์ฐ์๋ง ๋ชจํน ์ฌ์ฉ
์ฒดํฌ๋ฆฌ์คํธ
- ํ ์คํธ ํ๊ฒฝ ์ค์ ์๋ฃ
- ๊ธฐ๋ณธ ํ ์คํธ ์์ฑ ๊ฐ๋ฅ
- ๋น๋๊ธฐ ํ ์คํธ ์ดํด
- Mocking ํ์ฉ ๊ฐ๋ฅ
- React ์ปดํฌ๋ํธ ํ ์คํธ ๊ฐ๋ฅ
- ์ฝ๋ ์ปค๋ฒ๋ฆฌ์ง ์ธก์ ๊ฐ๋ฅ