๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Vitest๋กœ ์ž‘์„ฑ๋œ ํ…Œ์ŠคํŠธ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ๋””๋ฒ„๊น…ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

Vitest ๋””๋ฒ„๊น… ๋ฐฉ๋ฒ•

Sonamu๋Š” Vitest๋ฅผ ํ…Œ์ŠคํŠธ ํ”„๋ ˆ์ž„์›Œํฌ๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ๋””๋ฒ„๊น… ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

VSCode ๋””๋ฒ„๊ฑฐ ์‚ฌ์šฉ

1. launch.json ์„ค์ •

.vscode/launch.json:
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "node",
      "request": "launch",
      "name": "Debug Current Test File",
      "runtimeExecutable": "pnpm",
      "runtimeArgs": [
        "vitest",
        "run",
        "${file}"
      ],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "sourceMaps": true,
      "resolveSourceMapLocations": [
        "${workspaceFolder}/**",
        "!**/node_modules/**"
      ]
    },
    {
      "type": "node",
      "request": "launch",
      "name": "Debug All Tests",
      "runtimeExecutable": "pnpm",
      "runtimeArgs": [
        "vitest",
        "run"
      ],
      "cwd": "${workspaceFolder}",
      "console": "integratedTerminal",
      "sourceMaps": true
    }
  ]
}

2. ๋””๋ฒ„๊น… ์‹คํ–‰

  1. ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์—ด๊ธฐ
    • ๋””๋ฒ„๊น…ํ•˜๋ ค๋Š” .test.ts ํŒŒ์ผ ์—ด๊ธฐ
  2. ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ ์„ค์ •
    • ์ค„ ๋ฒˆํ˜ธ ์™ผ์ชฝ ํด๋ฆญํ•˜์—ฌ ๋นจ๊ฐ„ ์  ํ‘œ์‹œ
  3. ๋””๋ฒ„๊ฑฐ ์‹œ์ž‘
    • F5 ํ‚ค ๋˜๋Š” โ€œRun and Debugโ€ ํŒจ๋„์—์„œ โ€œDebug Current Test Fileโ€ ์„ ํƒ
  4. ๋””๋ฒ„๊น…
    • ๋ธŒ๋ ˆ์ดํฌํฌ์ธํŠธ์—์„œ ๋ฉˆ์ถ”๋ฉด ๋ณ€์ˆ˜ ๊ฒ€์‚ฌ
    • F10 (Step Over), F11 (Step Into) ๋“ฑ ์‚ฌ์šฉ

์ฝ˜์†” ๋กœ๊น…

console.log ์‚ฌ์šฉ

๊ฐ€์žฅ ๊ฐ„๋‹จํ•œ ๋””๋ฒ„๊น… ๋ฐฉ๋ฒ•:
test("์‚ฌ์šฉ์ž ์ƒ์„ฑ", async () => {
  const email = "[email protected]";
  
  console.log("ํ…Œ์ŠคํŠธ ์‹œ์ž‘:", email);
  
  const user = await UserModel.create({ email });
  console.log("์ƒ์„ฑ๋œ ์‚ฌ์šฉ์ž:", user);
  
  expect(user.email).toBe(email);
});

๊ตฌ์กฐํ™”๋œ ๋กœ๊น…

test("๋ณต์žกํ•œ ๊ฐ์ฒด ๋กœ๊น…", async () => {
  const data = { 
    user: { id: 1, email: "[email protected]" },
    items: [1, 2, 3]
  };
  
  // ์˜ˆ์˜๊ฒŒ ์ถœ๋ ฅ
  console.log(JSON.stringify(data, null, 2));
  
  // ๋˜๋Š”
  console.dir(data, { depth: null });
});

ํŠน์ • ํ…Œ์ŠคํŠธ๋งŒ ์‹คํ–‰

test.only

// โœ… ์ด ํ…Œ์ŠคํŠธ๋งŒ ์‹คํ–‰
test.only("๋””๋ฒ„๊น…ํ•  ํ…Œ์ŠคํŠธ", async () => {
  const result = await complexOperation();
  expect(result).toBeDefined();
});

// โŒ ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ๋Š” ๊ฑด๋„ˆ๋œ€
test("๊ฑด๋„ˆ๋›ธ ํ…Œ์ŠคํŠธ", async () => {
  // ...
});

describe.only

// โœ… ์ด ๋ธ”๋ก์˜ ํ…Œ์ŠคํŠธ๋“ค๋งŒ ์‹คํ–‰
describe.only("User API", () => {
  test("์‚ฌ์šฉ์ž ์ƒ์„ฑ", async () => {
    // ...
  });
  
  test("์‚ฌ์šฉ์ž ์กฐํšŒ", async () => {
    // ...
  });
});

// โŒ ๋‹ค๋ฅธ describe ๋ธ”๋ก์€ ๊ฑด๋„ˆ๋œ€
describe("Order API", () => {
  test("์ฃผ๋ฌธ ์ƒ์„ฑ", async () => {
    // ...
  });
});

CLI๋กœ ํŠน์ • ํ…Œ์ŠคํŠธ ์‹คํ–‰

# ํŠน์ • ํŒŒ์ผ๋งŒ
pnpm vitest run src/models/user.model.test.ts

# ํŠน์ • ํŒจํ„ด
pnpm vitest run user

# ํŠน์ • ํ…Œ์ŠคํŠธ ์ด๋ฆ„
pnpm vitest run -t "์‚ฌ์šฉ์ž ์ƒ์„ฑ"

Watch ๋ชจ๋“œ

์ž๋™ ์žฌ์‹คํ–‰

# Watch ๋ชจ๋“œ๋กœ ์‹คํ–‰
pnpm vitest watch

# ๋˜๋Š”
pnpm test --watch
ํŒŒ์ผ์„ ์ˆ˜์ •ํ•˜๋ฉด ๊ด€๋ จ ํ…Œ์ŠคํŠธ๊ฐ€ ์ž๋™์œผ๋กœ ์žฌ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

Watch ๋ชจ๋“œ ๋‹จ์ถ•ํ‚ค

ํ…Œ์ŠคํŠธ ์‹คํ–‰ ์ค‘:
  • a: ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์žฌ์‹คํ–‰
  • f: ์‹คํŒจํ•œ ํ…Œ์ŠคํŠธ๋งŒ ์žฌ์‹คํ–‰
  • t: ํŠน์ • ํ…Œ์ŠคํŠธ ํ•„ํ„ฐ
  • q: ์ข…๋ฃŒ

UI ๋ชจ๋“œ (vitest โ€”ui)

์‹คํ–‰

pnpm vitest --ui
๋ธŒ๋ผ์šฐ์ €์—์„œ http://localhost:51204/__vitest__/ ์—ด๋ฆผ ๊ธฐ๋Šฅ:
  • ํ…Œ์ŠคํŠธ ๊ณ„์ธต ๊ตฌ์กฐ ์‹œ๊ฐํ™”
  • ์‹คํŒจํ•œ ํ…Œ์ŠคํŠธ ํ•„ํ„ฐ๋ง
  • ๊ฐœ๋ณ„ ํ…Œ์ŠคํŠธ ์žฌ์‹คํ–‰
  • ์ฝ”๋“œ ์ปค๋ฒ„๋ฆฌ์ง€ ํ™•์ธ
  • ์ฝ˜์†” ์ถœ๋ ฅ ํ™•์ธ

์—๋Ÿฌ ๋””๋ฒ„๊น…

์ƒ์„ธํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€

test("์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๊ฐœ์„ ", async () => {
  const userId = 999;
  
  // โŒ ๊ธฐ๋ณธ ์—๋Ÿฌ
  expect(await UserModel.findById(userId)).toBeDefined();
  
  // โœ… ์ƒ์„ธํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€
  const user = await UserModel.findById(userId);
  expect(user, `์‚ฌ์šฉ์ž ID ${userId}๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ`).toBeDefined();
});

try-catch๋กœ ์—๋Ÿฌ ํ™•์ธ

test("์—๋Ÿฌ ์ƒ์„ธ ๋ถ„์„", async () => {
  try {
    await riskyOperation();
    expect.fail("์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•ด์•ผ ํ•จ");
  } catch (error) {
    // ์—๋Ÿฌ ์ƒ์„ธ ํ™•์ธ
    console.log("์—๋Ÿฌ ํƒ€์ž…:", error.constructor.name);
    console.log("์—๋Ÿฌ ๋ฉ”์‹œ์ง€:", error.message);
    console.log("์Šคํƒ ํŠธ๋ ˆ์ด์Šค:", error.stack);
    
    expect(error).toBeInstanceOf(BadRequestException);
  }
});

๋น„๋™๊ธฐ ์ฝ”๋“œ ๋””๋ฒ„๊น…

async/await ํ™•์ธ

test("๋น„๋™๊ธฐ ์ž‘์—… ๋””๋ฒ„๊น…", async () => {
  console.log("1. ์‹œ์ž‘");
  
  const promise1 = fetchUser(1);
  console.log("2. fetchUser ํ˜ธ์ถœ");
  
  const promise2 = fetchOrders(1);
  console.log("3. fetchOrders ํ˜ธ์ถœ");
  
  const [user, orders] = await Promise.all([promise1, promise2]);
  console.log("4. ๋ชจ๋‘ ์™„๋ฃŒ:", { user, orders });
  
  expect(user).toBeDefined();
});

ํƒ€์ž„์•„์›ƒ ์ฆ๊ฐ€

test("๋А๋ฆฐ ์ž‘์—…", async () => {
  // ๊ธฐ๋ณธ 5์ดˆ์—์„œ 30์ดˆ๋กœ ์ฆ๊ฐ€
  const result = await slowOperation();
  expect(result).toBeDefined();
}, 30000);  // 30์ดˆ ํƒ€์ž„์•„์›ƒ

๋ชจํ‚น ๋””๋ฒ„๊น…

Mock ํ˜ธ์ถœ ํ™•์ธ

import { vi } from "vitest";

test("Mock ํ˜ธ์ถœ ๋””๋ฒ„๊น…", async () => {
  const mockFn = vi.fn().mockResolvedValue({ id: 1 });
  
  await mockFn("test");
  await mockFn("test2");
  
  // Mock ํ˜ธ์ถœ ํ™•์ธ
  console.log("ํ˜ธ์ถœ ํšŸ์ˆ˜:", mockFn.mock.calls.length);
  console.log("์ฒซ ๋ฒˆ์งธ ํ˜ธ์ถœ:", mockFn.mock.calls[0]);
  console.log("๋ชจ๋“  ํ˜ธ์ถœ:", mockFn.mock.calls);
  
  expect(mockFn).toHaveBeenCalledTimes(2);
  expect(mockFn).toHaveBeenCalledWith("test");
});

Mock ๋ฆฌ์…‹ ํ™•์ธ

beforeEach(() => {
  // ๊ฐ ํ…Œ์ŠคํŠธ ์ „์— Mock ๋ฆฌ์…‹
  vi.clearAllMocks();
});

test("Mock ์ƒํƒœ ํ™•์ธ", () => {
  const mockFn = vi.fn();
  
  // ํ˜ธ์ถœ ์ „
  console.log("ํ˜ธ์ถœ ์ „:", mockFn.mock.calls.length); // 0
  
  mockFn("test");
  
  // ํ˜ธ์ถœ ํ›„
  console.log("ํ˜ธ์ถœ ํ›„:", mockFn.mock.calls.length); // 1
});

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒํƒœ ํ™•์ธ

์ฟผ๋ฆฌ ๋กœ๊น…

test("DB ์ฟผ๋ฆฌ ํ™•์ธ", async () => {
  // ์ฟผ๋ฆฌ ๋กœ๊น… ํ™œ์„ฑํ™” (๊ฐœ๋ฐœ ์ค‘)
  const result = await UserModel.getPuri()
    .select("*")
    .where({ email: "[email protected]" })
    .debug()  // ์ฟผ๋ฆฌ ์ถœ๋ ฅ
    .getOne();
  
  console.log("๊ฒฐ๊ณผ:", result);
});

ํ…Œ์ŠคํŠธ ํ›„ ๋ฐ์ดํ„ฐ ํ™•์ธ

test("๋ฐ์ดํ„ฐ ์ƒ์„ฑ ํ™•์ธ", async () => {
  const email = "[email protected]";
  
  await UserModel.create({ email });
  
  // DB์—์„œ ์ง์ ‘ ํ™•์ธ
  const users = await UserModel.getPuri()
    .select("*")
    .getMany();
  
  console.log("์ „์ฒด ์‚ฌ์šฉ์ž:", users);
  
  const created = users.find(u => u.email === email);
  console.log("์ƒ์„ฑ๋œ ์‚ฌ์šฉ์ž:", created);
  
  expect(created).toBeDefined();
});

Coverage ๋””๋ฒ„๊น…

์ปค๋ฒ„๋ฆฌ์ง€ ํ™•์ธ

# ์ปค๋ฒ„๋ฆฌ์ง€ ์ƒ์„ฑ
pnpm test --coverage

# HTML ๋ฆฌํฌํŠธ ์—ด๊ธฐ
open coverage/index.html

์ปค๋ฒ„๋˜์ง€ ์•Š์€ ์ฝ”๋“œ ์ฐพ๊ธฐ

// src/models/user.model.ts
class UserModelClass extends BaseModel {
  async createUser(email: string) {
    if (!email) {
      // โŒ ์ด ๋ถ„๊ธฐ๊ฐ€ ํ…Œ์ŠคํŠธ๋˜์ง€ ์•Š์Œ
      throw new Error("Email required");
    }
    
    return this.save({ email });
  }
}

// ํ…Œ์ŠคํŠธ ์ถ”๊ฐ€ ํ•„์š”
test("์ด๋ฉ”์ผ ์—†์ด ์ƒ์„ฑ ์‹œ ์—๋Ÿฌ", async () => {
  await expect(
    UserModel.createUser("")
  ).rejects.toThrow("Email required");
});

Vitest ์„ค์ • ๋””๋ฒ„๊น…

์„ค์ • ํ™•์ธ

vitest.config.ts:
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    include: ["src/**/*.test.ts"],
    
    // ๋””๋ฒ„๊น… ์˜ต์…˜
    bail: 1,  // ์ฒซ ์‹คํŒจ ์‹œ ์ค‘๋‹จ
    reporters: ["verbose"],  // ์ƒ์„ธ ์ถœ๋ ฅ
    logHeapUsage: true,  // ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ถœ๋ ฅ
    
    // ํƒ€์ž„์•„์›ƒ ์ฆ๊ฐ€
    testTimeout: 10000,
    hookTimeout: 10000
  }
});

ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ํ™•์ธ

test("ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋””๋ฒ„๊น…", () => {
  console.log("NODE_ENV:", process.env.NODE_ENV);
  console.log("DB_HOST:", process.env.DB_HOST);
  console.log("๋ชจ๋“  ํ™˜๊ฒฝ ๋ณ€์ˆ˜:", process.env);
});

๋ณ‘๋ ฌ ์‹คํ–‰ ๋””๋ฒ„๊น…

๊ฒฉ๋ฆฌ ๋ชจ๋“œ ๋น„ํ™œ์„ฑํ™”

์ผ๋ถ€ ํ…Œ์ŠคํŠธ๋Š” ๋ณ‘๋ ฌ ์‹คํ–‰ ์‹œ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค:
// vitest.config.ts
export default defineConfig({
  test: {
    pool: "forks",
    maxWorkers: 1,  // ๋‹จ์ผ ์›Œ์ปค
    isolate: false  // ๊ฒฉ๋ฆฌ ๋น„ํ™œ์„ฑํ™”
  }
});

์ˆœ์ฐจ ์‹คํ–‰

# ์ˆœ์ฐจ ์‹คํ–‰
pnpm vitest run --no-threads

์Šค๋ƒ…์ƒท ๋””๋ฒ„๊น…

์Šค๋ƒ…์ƒท ์—…๋ฐ์ดํŠธ

# ์Šค๋ƒ…์ƒท ์—…๋ฐ์ดํŠธ
pnpm vitest run -u

# ํŠน์ • ํŒŒ์ผ๋งŒ
pnpm vitest run -u src/models/user.model.test.ts

์Šค๋ƒ…์ƒท ํ™•์ธ

test("์Šค๋ƒ…์ƒท ๋””๋ฒ„๊น…", () => {
  const data = {
    id: 1,
    name: "Test",
    timestamp: new Date().toISOString()
  };
  
  // โŒ timestamp๊ฐ€ ๊ณ„์† ๋ณ€๊ฒฝ๋จ
  expect(data).toMatchSnapshot();
  
  // โœ… ๋™์  ๊ฐ’ ์ œ์™ธ
  expect({
    ...data,
    timestamp: expect.any(String)
  }).toMatchSnapshot();
});

ํƒ€์ž… ์ฒดํฌ ๋””๋ฒ„๊น…

ํƒ€์ž… ์—๋Ÿฌ ํ™•์ธ

# ํƒ€์ž… ์ฒดํฌ๋งŒ ์‹คํ–‰
pnpm vitest typecheck

# ๋˜๋Š”
pnpm tsc --noEmit

ํƒ€์ž… ํ…Œ์ŠคํŠธ

// src/**/*type-safety.test.ts
import { expectTypeOf } from "vitest";

test("ํƒ€์ž… ์•ˆ์ „์„ฑ ํ™•์ธ", () => {
  const user = { id: 1, email: "[email protected]" };
  
  expectTypeOf(user.id).toEqualTypeOf<number>();
  expectTypeOf(user.email).toEqualTypeOf<string>();
});

ํ…Œ์ŠคํŠธ ๊ฒฉ๋ฆฌ ๋ฌธ์ œ

beforeEach/afterEach ํ™•์ธ

describe("User Tests", () => {
  let testUser: User;
  
  beforeEach(async () => {
    console.log("Setup ์‹œ์ž‘");
    testUser = await UserModel.create({ 
      email: "[email protected]" 
    });
    console.log("Setup ์™„๋ฃŒ:", testUser.id);
  });
  
  afterEach(async () => {
    console.log("Cleanup ์‹œ์ž‘");
    if (testUser) {
      await UserModel.del(testUser.id);
    }
    console.log("Cleanup ์™„๋ฃŒ");
  });
  
  test("ํ…Œ์ŠคํŠธ 1", async () => {
    expect(testUser).toBeDefined();
  });
});

๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋””๋ฒ„๊น…

๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ํ™•์ธ

// vitest.config.ts
export default defineConfig({
  test: {
    logHeapUsage: true  // ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ถœ๋ ฅ
  }
});

๋ฆฌ์†Œ์Šค ์ •๋ฆฌ ํ™•์ธ

test("๋ฆฌ์†Œ์Šค ์ •๋ฆฌ ํ™•์ธ", async () => {
  const db = createConnection();
  
  try {
    await db.query("SELECT 1");
  } finally {
    await db.destroy();  // ๋ฐ˜๋“œ์‹œ ์ •๋ฆฌ
    console.log("DB ์—ฐ๊ฒฐ ์ •๋ฆฌ ์™„๋ฃŒ");
  }
});

Best Practices

1. ์ž‘์€ ๋‹จ์œ„๋กœ ํ…Œ์ŠคํŠธ

// โŒ ๋„ˆ๋ฌด ํฐ ํ…Œ์ŠคํŠธ
test("์ „์ฒด ์‚ฌ์šฉ์ž ํ”Œ๋กœ์šฐ", async () => {
  // ์ƒ์„ฑ, ์กฐํšŒ, ์ˆ˜์ •, ์‚ญ์ œ ๋ชจ๋‘ ํฌํ•จ
});

// โœ… ์ž‘์€ ๋‹จ์œ„๋กœ ๋ถ„๋ฆฌ
test("์‚ฌ์šฉ์ž ์ƒ์„ฑ", async () => { });
test("์‚ฌ์šฉ์ž ์กฐํšŒ", async () => { });
test("์‚ฌ์šฉ์ž ์ˆ˜์ •", async () => { });
test("์‚ฌ์šฉ์ž ์‚ญ์ œ", async () => { });

2. ์˜๋ฏธ ์žˆ๋Š” ํ…Œ์ŠคํŠธ ์ด๋ฆ„

// โŒ ๋ถˆ๋ช…ํ™•ํ•œ ์ด๋ฆ„
test("test1", async () => { });

// โœ… ๋ช…ํ™•ํ•œ ์ด๋ฆ„
test("์ด๋ฉ”์ผ ์—†์ด ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์‹œ BadRequestException ๋ฐœ์ƒ", async () => {
  await expect(
    UserModel.create({ email: "" })
  ).rejects.toThrow(BadRequestException);
});

3. AAA ํŒจํ„ด (Arrange-Act-Assert)

test("์‚ฌ์šฉ์ž ์ƒ์„ฑ", async () => {
  // Arrange (์ค€๋น„)
  const email = "[email protected]";
  const name = "Test User";
  
  // Act (์‹คํ–‰)
  const user = await UserModel.create({ email, name });
  
  // Assert (๊ฒ€์ฆ)
  expect(user.email).toBe(email);
  expect(user.name).toBe(name);
});

4. ์‹คํŒจ ์‹œ ์ถฉ๋ถ„ํ•œ ์ •๋ณด

test("๋ณต์žกํ•œ ์กฐ๊ฑด ๊ฒ€์ฆ", async () => {
  const result = await complexCalculation();
  
  // โœ… ์‹คํŒจ ์‹œ ๋ฌด์—‡์ด ์ž˜๋ชป๋˜์—ˆ๋Š”์ง€ ๋ช…ํ™•ํžˆ
  expect(
    result.total,
    `total์ด ${result.expected}์—ฌ์•ผ ํ•˜๋Š”๋ฐ ${result.total}์ž„`
  ).toBe(result.expected);
});

๊ด€๋ จ ๋ฌธ์„œ