์ด ๋ฌธ์์์๋ Altsis ํ๋ก์ ํธ์ ํ ์คํธ ์คํ ๋ฐฉ๋ฒ, ์์ฑ ๊ฐ์ด๋, ๋๋ฒ๊น ๋ฐฉ๋ฒ์ ์ค๋ช ํฉ๋๋ค.
- ํ ์คํธ ํ๋ ์์ํฌ
- ํ ์คํธ ์คํ
- ๋ฐฑ์๋ ํ ์คํธ ์์ฑ
- ํ๋ก ํธ์๋ ํ ์คํธ ์์ฑ
- ํ ์คํธ ๋๋ฒ๊น
- ํ ์คํธ ๋ชจ๋ฒ ์ฌ๋ก
| ์์ญ | ํ๋ ์์ํฌ | ์ถ๊ฐ ๋๊ตฌ |
|---|---|---|
| ๋ฐฑ์๋ | Jest 29.x | node-mocks-http (Express req/res ๋ชจํน), babel-jest |
| ํ๋ก ํธ์๋ | Jest (react-scripts) | @testing-library/react, @testing-library/jest-dom, @testing-library/user-event |
๋ฐฑ์๋ ํ ์คํธ๋ Babel์ ํตํด ES Module์ CommonJS๋ก ๋ณํํ์ฌ ์คํ๋ฉ๋๋ค:
jest.config.json- Jest ์ค์ @babel/preset-env+@babel/plugin-transform-modules-commonjs- ES Module ๋ณํNODE_ENV=test์.env.testํ๊ฒฝ ๋ณ์ ์ฌ์ฉ
ํ๋ก ํธ์๋ ํ
์คํธ๋ react-scripts test๋ฅผ ํตํด ์คํ๋ฉ๋๋ค:
- Create React App ๋ด์ฅ Jest ์ค์ ์ฌ์ฉ
@testing-library/react- React ์ปดํฌ๋ํธ ํ ์คํธ ์ ํธ๋ฆฌํฐ@testing-library/jest-dom- DOM ๋งค์ฒ ํ์ฅ
cd backend
# ์ ์ฒด ํ
์คํธ ์คํ
yarn test
# ํน์ ํ
์คํธ ํ์ผ๋ง ์คํ
yarn test -- --testPathPattern="seasons"
# ํ๋ก๋์
ํ๊ฒฝ ํ
์คํธ
yarn test --prod๋ฐฑ์๋ yarn test ๋ช
๋ น์ ๋ค์๊ณผ ๊ฐ์ด ์คํ๋ฉ๋๋ค:
jest -c ./jest.config.json --detectOpenHandles --forceExit
--detectOpenHandles: ์ด๋ ค ์๋ ํธ๋ค(DB ์ฐ๊ฒฐ ๋ฑ) ๊ฐ์ง--forceExit: ํ ์คํธ ์๋ฃ ํ ๊ฐ์ ์ข ๋ฃ (๋น๋๊ธฐ ํธ๋ค ์ ๋ฆฌ)
cd frontend
# ์ ์ฒด ํ
์คํธ ์คํ
yarn test
# ํน์ ํ
์คํธ๋ง ๊ฐ์ ๋ชจ๋๋ก ์คํ
yarn test --watch TestName
# ์ปค๋ฒ๋ฆฌ์ง ํฌํจ ์คํ
yarn test -- --coverage| ์์น | ๋ช ๋ น์ด | ์ค๋ช |
|---|---|---|
backend/ |
yarn test |
์ ์ฒด ๋ฐฑ์๋ ํ ์คํธ (Jest) |
backend/ |
yarn test --prod |
ํ๋ก๋์ ํ๊ฒฝ ํ ์คํธ |
frontend/ |
yarn test |
์ ์ฒด ํ๋ก ํธ์๋ ํ ์คํธ |
frontend/ |
yarn test --watch TestName |
ํน์ ํ ์คํธ ๊ฐ์ ๋ชจ๋ |
node-mocks-http๋ฅผ ์ฌ์ฉํ์ฌ Express์ req/res ๊ฐ์ฒด๋ฅผ ๋ชจํนํฉ๋๋ค:
import httpMocks from "node-mocks-http";
import { create } from "../controllers/seasons.js";
describe("SeasonController", () => {
describe("create (CSeason)", () => {
it("ํ์ ํ๋๊ฐ ์์ผ๋ฉด 400์ ๋ฐํํด์ผ ํ๋ค", async () => {
const req = httpMocks.createRequest({
method: "POST",
url: "/api/seasons",
body: {
// school ํ๋ ๋๋ฝ
year: "2024",
term: "1ํ๊ธฐ",
},
user: {
academyId: "testAcademy",
auth: "admin",
},
});
const res = httpMocks.createResponse();
await create(req, res);
expect(res.statusCode).toBe(400);
expect(res._getJSONData().message).toBe("SCHOOL_REQUIRED");
});
it("์ ํจํ ์์ฒญ์ด๋ฉด 200๊ณผ season ๊ฐ์ฒด๋ฅผ ๋ฐํํด์ผ ํ๋ค", async () => {
const req = httpMocks.createRequest({
method: "POST",
url: "/api/seasons",
body: {
school: "64a1b2c3d4e5f6g7h8i9j0k1",
year: "2024",
term: "1ํ๊ธฐ",
period: { start: "2024-03-01", end: "2024-07-31" },
},
user: {
academyId: "testAcademy",
auth: "admin",
},
});
const res = httpMocks.createResponse();
await create(req, res);
expect(res.statusCode).toBe(200);
const data = res._getJSONData();
expect(data.season).toBeDefined();
expect(data.season.year).toBe("2024");
});
});
});import mongoose from "mongoose";
import { Season } from "../models/Season.js";
describe("Season Model", () => {
let connection;
beforeAll(async () => {
// ํ
์คํธ์ฉ DB ์ฐ๊ฒฐ
connection = await mongoose.createConnection(process.env.TEST_DB_URL);
});
afterAll(async () => {
await connection.close();
});
it("ํ์ ํ๋๊ฐ ์์ผ๋ฉด Season์ ์์ฑํ ์ ์์ด์ผ ํ๋ค", async () => {
const seasonData = {
school: new mongoose.Types.ObjectId(),
schoolId: "testSchool",
schoolName: "ํ
์คํธ ํ๊ต",
year: "2024",
term: "1ํ๊ธฐ",
};
const SeasonModel = connection.model("Season", Season.schema);
const season = new SeasonModel(seasonData);
const savedSeason = await season.save();
expect(savedSeason._id).toBeDefined();
expect(savedSeason.year).toBe("2024");
expect(savedSeason.isActivated).toBe(false); // ๊ธฐ๋ณธ๊ฐ
});
});import { SeasonService } from "../services/seasons.js";
describe("SeasonService", () => {
it("ํ๊ธฐ ํ์ฑํ ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฒ๋ฆฌ๋์ด์ผ ํ๋ค", async () => {
// Given
const dbName = "testAcademy";
const seasonId = "64a1b2c3d4e5f6g7h8i9j0k1";
// When
const result = await SeasonService.onActivate(dbName, seasonId);
// Then
expect(result.isActivated).toBe(true);
});
});import { render, screen } from "@testing-library/react";
import "@testing-library/jest-dom";
import MyComponent from "./MyComponent";
describe("MyComponent", () => {
it("์ ๋ชฉ์ด ์ฌ๋ฐ๋ฅด๊ฒ ๋ ๋๋ง๋์ด์ผ ํ๋ค", () => {
render(<MyComponent title="ํ
์คํธ ์ ๋ชฉ" />);
expect(screen.getByText("ํ
์คํธ ์ ๋ชฉ")).toBeInTheDocument();
});
it("๋ฐ์ดํฐ๊ฐ ์์ ๋ ๋น ์ํ ๋ฉ์์ง๋ฅผ ํ์ํด์ผ ํ๋ค", () => {
render(<MyComponent title="ํ
์คํธ" data={[]} />);
expect(screen.getByText("๋ฐ์ดํฐ๊ฐ ์์ต๋๋ค.")).toBeInTheDocument();
});
});import { render, screen, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import MyComponent from "./MyComponent";
describe("MyComponent ์ํธ์์ฉ", () => {
it("๋ฒํผ ํด๋ฆญ ์ ํธ๋ค๋ฌ๊ฐ ํธ์ถ๋์ด์ผ ํ๋ค", async () => {
const handleClick = jest.fn();
render(<MyComponent onClick={handleClick} />);
await userEvent.click(screen.getByRole("button", { name: "ํ์ธ" }));
expect(handleClick).toHaveBeenCalledTimes(1);
});
it("์
๋ ฅ๊ฐ ๋ณ๊ฒฝ ์ ์ํ๊ฐ ์
๋ฐ์ดํธ๋์ด์ผ ํ๋ค", async () => {
render(<MyComponent />);
const input = screen.getByPlaceholderText("์ด๋ฆ์ ์
๋ ฅํ์ธ์");
await userEvent.type(input, "ํ๊ธธ๋");
expect(input).toHaveValue("ํ๊ธธ๋");
});
});import { render, screen, waitFor } from "@testing-library/react";
import MyComponent from "./MyComponent";
describe("MyComponent ๋น๋๊ธฐ", () => {
it("๋ฐ์ดํฐ ๋ก๋ฉ ํ ๋ชฉ๋ก์ด ํ์๋์ด์ผ ํ๋ค", async () => {
render(<MyComponent />);
// ๋ก๋ฉ ์ํ ํ์ธ
expect(screen.getByText("๋ก๋ฉ ์ค...")).toBeInTheDocument();
// ๋ฐ์ดํฐ ๋ก๋ฉ ์๋ฃ ๋๊ธฐ
await waitFor(() => {
expect(screen.getByText("ํญ๋ชฉ 1")).toBeInTheDocument();
});
expect(screen.queryByText("๋ก๋ฉ ์ค...")).not.toBeInTheDocument();
});
});Node.js์ --inspect ํ๋๊ทธ๋ฅผ ์ฌ์ฉํ์ฌ Chrome DevTools์์ ํ
์คํธ๋ฅผ ๋๋ฒ๊น
ํ ์ ์์ต๋๋ค.
cd frontend
# ๋๋ฒ๊ฑฐ ์ฐ๋ ํ
์คํธ (๊ฐ์ ๋ชจ๋)
yarn debug-test --watch TestName- ์ ๋ช ๋ น์ด๋ฅผ ์คํํฉ๋๋ค.
- Chrome ๋ธ๋ผ์ฐ์ ์์
chrome://inspect๋ฅผ ์ฝ๋๋ค. - "Remote Target" ์น์ ์์ Node.js ํ๋ก์ธ์ค๋ฅผ ์ฐพ์ "inspect"๋ฅผ ํด๋ฆญํฉ๋๋ค.
- DevTools๊ฐ ์ด๋ฆฌ๋ฉด Sources ํญ์์ ๋ธ๋ ์ดํฌํฌ์ธํธ๋ฅผ ์ค์ ํ ์ ์์ต๋๋ค.
ํ
์คํธ ์ฝ๋์ debugger ๋ฌธ์ ์ถ๊ฐํ๋ฉด ํด๋น ์ง์ ์์ ์คํ์ด ๋ฉ์ถฅ๋๋ค:
it("๋๋ฒ๊น
์ด ํ์ํ ํ
์คํธ", async () => {
const result = await someFunction();
debugger; // ์ฌ๊ธฐ์ ์คํ์ด ๋ฉ์ถค
expect(result).toBe(expected);
});VS Code์์ ์ง์ ํ
์คํธ๋ฅผ ๋๋ฒ๊น
ํ ์๋ ์์ต๋๋ค. .vscode/launch.json์ ๋ค์ ์ค์ ์ ์ถ๊ฐํฉ๋๋ค:
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Jest Tests (Backend)",
"type": "node",
"request": "launch",
"runtimeExecutable": "yarn",
"runtimeArgs": ["test", "--runInBand"],
"cwd": "${workspaceFolder}/backend",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
},
{
"name": "Debug Jest Tests (Frontend)",
"type": "node",
"request": "launch",
"runtimeExecutable": "yarn",
"runtimeArgs": [
"test",
"--runInBand",
"--no-cache",
"--watchAll=false"
],
"cwd": "${workspaceFolder}/frontend",
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}it("์ค๋ช
์ ์ธ ํ
์คํธ ์ด๋ฆ", async () => {
// Arrange (์ค๋น)
const input = { year: "2024", term: "1ํ๊ธฐ" };
// Act (์คํ)
const result = await createSeason(input);
// Assert (๊ฒ์ฆ)
expect(result.year).toBe("2024");
expect(result.isActivated).toBe(false);
});describe("SeasonController", () => {
describe("create", () => {
it("์ ํจํ ๋ฐ์ดํฐ๋ก ํ๊ธฐ๋ฅผ ์์ฑํ ์ ์์ด์ผ ํ๋ค", () => { ... });
it("ํ์ ํ๋๊ฐ ์์ผ๋ฉด 400์ ๋ฐํํด์ผ ํ๋ค", () => { ... });
it("์ค๋ณต ํ๊ธฐ ์์ฑ ์ 409๋ฅผ ๋ฐํํด์ผ ํ๋ค", () => { ... });
});
describe("find", () => {
it("ํ๊ธฐ ๋ชฉ๋ก์ ์กฐํํ ์ ์์ด์ผ ํ๋ค", () => { ... });
it("ํน์ ํ๊ธฐ๋ฅผ ID๋ก ์กฐํํ ์ ์์ด์ผ ํ๋ค", () => { ... });
});
describe("remove", () => {
it("ํ๊ธฐ๋ฅผ ์ญ์ ํ ์ ์์ด์ผ ํ๋ค", () => { ... });
it("์กด์ฌํ์ง ์๋ ํ๊ธฐ ์ญ์ ์ 404๋ฅผ ๋ฐํํด์ผ ํ๋ค", () => { ... });
});
});๊ฐ ํ ์คํธ๋ ๋ ๋ฆฝ์ ์ด์ด์ผ ํฉ๋๋ค. ํ ์คํธ ๊ฐ ์ํ ๊ณต์ ๋ฅผ ํผํ์ญ์์ค:
describe("UserService", () => {
// ๊ฐ ํ
์คํธ ์ ์ ๋ฐ์ดํฐ ์ด๊ธฐํ
beforeEach(async () => {
await clearTestData();
});
// ๋ชจ๋ ํ
์คํธ ํ ์ฐ๊ฒฐ ์ ๋ฆฌ
afterAll(async () => {
await closeConnections();
});
it("ํ
์คํธ 1", () => { ... });
it("ํ
์คํธ 2", () => { ... }); // ํ
์คํธ 1์ ์ํฅ์ ๋ฐ์ง ์์
});ํ ์คํธ ์ด๋ฆ์ ํ๊ตญ์ด๋ก ์์ฑํ๋ฉฐ, ๋ฌด์์ ํ ์คํธํ๋์ง ๋ช ํํ๊ฒ ํํํฉ๋๋ค:
// ์ฌ๋ฐ๋ฅธ ์์ - ๊ตฌ์ฒด์ ์ด๊ณ ๋ช
ํํ ์ด๋ฆ
it("๊ด๋ฆฌ์๊ฐ ํ๊ธฐ๋ฅผ ์์ฑํ๋ฉด ๋นํ์ฑํ ์ํ๋ก ์ ์ฅ๋์ด์ผ ํ๋ค", () => { ... });
it("ํ์ ํ๋(school)๊ฐ ๋๋ฝ๋๋ฉด SCHOOL_REQUIRED ์๋ฌ๋ฅผ ๋ฐํํด์ผ ํ๋ค", () => { ... });
it("์ด๋ฏธ ์กด์ฌํ๋ ๋
๋/ํ๊ธฐ ์กฐํฉ์ผ๋ก ์์ฑ ์ 409๋ฅผ ๋ฐํํด์ผ ํ๋ค", () => { ... });
// ์๋ชป๋ ์์ - ๋ชจํธํ ์ด๋ฆ
it("ํ
์คํธ 1", () => { ... });
it("์ ์ ๋์", () => { ... });
it("์๋ฌ ์ฒ๋ฆฌ", () => { ... });# ํ๋ก ํธ์๋ ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ
cd frontend
yarn test -- --coverage --watchAll=false
# ๋ฐฑ์๋ ์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ
cd backend
yarn test -- --coverage์ปค๋ฒ๋ฆฌ์ง ๋ฆฌํฌํธ๋ coverage/ ๋๋ ํ ๋ฆฌ์ ์์ฑ๋ฉ๋๋ค. coverage/lcov-report/index.html์ ๋ธ๋ผ์ฐ์ ์์ ์ด์ด ์๊ฐ์ ์ผ๋ก ํ์ธํ ์ ์์ต๋๋ค.