Y J S

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");
  });
});

๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

  1. ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์ž‘์„ฑ: ๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์— ์˜์กดํ•˜์ง€ ์•Š์•„์•ผ ํ•จ
  2. ๋ช…ํ™•ํ•œ ํ…Œ์ŠคํŠธ ์ด๋ฆ„: it('should ...') ํ˜•์‹์œผ๋กœ ์˜๋„๋ฅผ ๋ช…ํ™•ํžˆ
  3. AAA ํŒจํ„ด: Arrange(์ค€๋น„) โ†’ Act(์‹คํ–‰) โ†’ Assert(๊ฒ€์ฆ)
  4. ํ•œ ๋ฒˆ์— ํ•˜๋‚˜๋งŒ ํ…Œ์ŠคํŠธ: ํ•˜๋‚˜์˜ ํ…Œ์ŠคํŠธ๋Š” ํ•˜๋‚˜์˜ ๋™์ž‘๋งŒ ๊ฒ€์ฆ
  5. Mock์€ ์ตœ์†Œํ™”: ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ๋ชจํ‚น ์‚ฌ์šฉ

์ฒดํฌ๋ฆฌ์ŠคํŠธ

  • ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์„ค์ • ์™„๋ฃŒ
  • ๊ธฐ๋ณธ ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๊ฐ€๋Šฅ
  • ๋น„๋™๊ธฐ ํ…Œ์ŠคํŠธ ์ดํ•ด
  • Mocking ํ™œ์šฉ ๊ฐ€๋Šฅ
  • React ์ปดํฌ๋„ŒํŠธ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
  • ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ์ธก์ • ๊ฐ€๋Šฅ

์ฐธ๊ณ