๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu๋Š” pnpm generate ๋ช…๋ น์–ด๋กœ ๋ชจ๋ธ ํ…Œ์ŠคํŠธ ํŒŒ์ผ์„ ์ž๋™ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

Scaffolding ๊ฐœ์š”

์ž๋™ ์ƒ์„ฑ

model.test.ts ์ƒ์„ฑ๊ธฐ๋ณธ ๊ตฌ์กฐ ํฌํ•จ

bootstrap ํฌํ•จ

ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์ž๋™ ์„ค์ •์ฆ‰์‹œ ์‹คํ–‰ ๊ฐ€๋Šฅ

CRUD ํ…Œ์ŠคํŠธ

๊ธฐ๋ณธ ํ…Œ์ŠคํŠธ ์Šค์ผˆ๋ ˆํ†ค์ปค์Šคํ„ฐ๋งˆ์ด์ง• ๊ฐ€๋Šฅ

ํƒ€์ž… ์•ˆ์ „

๋ชจ๋ธ๊ณผ ์—ฐ๋™์ž๋™์™„์„ฑ ์ง€์›

ํ…Œ์ŠคํŠธ ํŒŒ์ผ ์ƒ์„ฑ

๋ช…๋ น์–ด

pnpm generate
Entity๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉด ์ž๋™์œผ๋กœ {model}.test.ts ํŒŒ์ผ์ด ์ƒ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์ƒ์„ฑ ์œ„์น˜:
api/src/models/
โ”œโ”€โ”€ user.model.ts
โ”œโ”€โ”€ user.model.test.ts       # ์ž๋™ ์ƒ์„ฑ๋จ
โ”œโ”€โ”€ post.model.ts
โ””โ”€โ”€ post.model.test.ts        # ์ž๋™ ์ƒ์„ฑ๋จ

์ƒ์„ฑ๋œ ํŒŒ์ผ ๊ตฌ์กฐ

// api/src/models/user.model.test.ts
import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { UserModel } from "./user.model";

// ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ์ดˆ๊ธฐํ™”
bootstrap(vi);

test("์‚ฌ์šฉ์ž ์ƒ์„ฑ", async () => {
  const userModel = new UserModel();
  
  // ํ…Œ์ŠคํŠธ ์ฝ”๋“œ ์ž‘์„ฑ
  const { user } = await userModel.create({
    username: "john",
    email: "[email protected]",
    password: "password123",
  });
  
  expect(user.id).toBeGreaterThan(0);
  expect(user.username).toBe("john");
});

test("์‚ฌ์šฉ์ž ์กฐํšŒ", async () => {
  const userModel = new UserModel();
  
  // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
  const { user } = await userModel.create({
    username: "jane",
    email: "[email protected]",
    password: "password123",
  });
  
  // ์กฐํšŒ ํ…Œ์ŠคํŠธ
  const { user: found } = await userModel.getUser("C", user.id);
  
  expect(found.id).toBe(user.id);
  expect(found.username).toBe("jane");
});

ํ…Œ์ŠคํŠธ ์‹คํ–‰

๋‹จ์ผ ํŒŒ์ผ ์‹คํ–‰

# ํŠน์ • ๋ชจ๋ธ ํ…Œ์ŠคํŠธ๋งŒ ์‹คํ–‰
pnpm vitest user.model.test.ts

์ „์ฒด ํ…Œ์ŠคํŠธ ์‹คํ–‰

# ๋ชจ๋“  ํ…Œ์ŠคํŠธ ์‹คํ–‰
pnpm test

# ๋˜๋Š”
pnpm vitest

Watch ๋ชจ๋“œ

# ํŒŒ์ผ ๋ณ€๊ฒฝ ์‹œ ์ž๋™ ์žฌ์‹คํ–‰
pnpm vitest --watch

ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ๊ฐ€์ด๋“œ

CRUD ํ…Œ์ŠคํŠธ ํŒจํ„ด

import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { PostModel } from "./post.model";

bootstrap(vi);

// Create
test("๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ", async () => {
  const postModel = new PostModel();
  
  const { post } = await postModel.create({
    title: "Test Post",
    content: "Test Content",
    author_id: 1,
  });
  
  expect(post.id).toBeGreaterThan(0);
  expect(post.title).toBe("Test Post");
});

// Read
test("๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ", async () => {
  const postModel = new PostModel();
  
  // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
  const { post } = await postModel.create({
    title: "Test Post",
    content: "Test Content",
    author_id: 1,
  });
  
  // ์กฐํšŒ
  const { post: found } = await postModel.getPost("C", post.id);
  
  expect(found.id).toBe(post.id);
  expect(found.title).toBe("Test Post");
});

// Update
test("๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •", async () => {
  const postModel = new PostModel();
  
  // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
  const { post } = await postModel.create({
    title: "Original Title",
    content: "Original Content",
    author_id: 1,
  });
  
  // ์ˆ˜์ •
  await postModel.update(post.id, {
    title: "Updated Title",
  });
  
  // ํ™•์ธ
  const { post: updated } = await postModel.getPost("C", post.id);
  expect(updated.title).toBe("Updated Title");
  expect(updated.content).toBe("Original Content"); // ๋ณ€๊ฒฝ ์•ˆ ๋จ
});

// Delete
test("๊ฒŒ์‹œ๊ธ€ ์‚ญ์ œ", async () => {
  const postModel = new PostModel();
  
  // ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
  const { post } = await postModel.create({
    title: "Test Post",
    content: "Test Content",
    author_id: 1,
  });
  
  // ์‚ญ์ œ
  await postModel.delete(post.id);
  
  // ํ™•์ธ
  const deleted = await postModel.findById("C", post.id);
  expect(deleted).toBeNull();
});

๊ด€๊ณ„ ํ…Œ์ŠคํŠธ

test("์‚ฌ์šฉ์ž์™€ ๊ฒŒ์‹œ๊ธ€ ๊ด€๊ณ„", async () => {
  const userModel = new UserModel();
  const postModel = new PostModel();
  
  // ์‚ฌ์šฉ์ž ์ƒ์„ฑ
  const { user } = await userModel.create({
    username: "author",
    email: "[email protected]",
    password: "password",
  });
  
  // ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ
  const { post } = await postModel.create({
    title: "User's Post",
    content: "Content",
    author_id: user.id,
  });
  
  // ๊ด€๊ณ„ ํ™•์ธ
  expect(post.author_id).toBe(user.id);
  
  // Subset "C"๋กœ ์กฐํšŒํ•˜๋ฉด author ์ •๋ณด ํฌํ•จ
  const { post: fullPost } = await postModel.getPost("C", post.id);
  expect(fullPost.author?.username).toBe("author");
});

์—๋Ÿฌ ํ…Œ์ŠคํŠธ

test("์ค‘๋ณต ์ด๋ฉ”์ผ ๊ฒ€์ฆ", async () => {
  const userModel = new UserModel();
  
  // ์ฒซ ๋ฒˆ์งธ ์‚ฌ์šฉ์ž ์ƒ์„ฑ
  await userModel.create({
    username: "user1",
    email: "[email protected]",
    password: "password",
  });
  
  // ๊ฐ™์€ ์ด๋ฉ”์ผ๋กœ ๋‹ค์‹œ ์ƒ์„ฑ ์‹œ๋„
  await expect(
    userModel.create({
      username: "user2",
      email: "[email protected]",
      password: "password",
    })
  ).rejects.toThrow("์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ด๋ฉ”์ผ์ž…๋‹ˆ๋‹ค");
});

test("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฒŒ์‹œ๊ธ€ ์กฐํšŒ", async () => {
  const postModel = new PostModel();
  
  await expect(
    postModel.getPost("C", 999999)
  ).rejects.toThrow("๊ฒŒ์‹œ๊ธ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค");
});

ํ…Œ์ŠคํŠธ ๊ตฌ์กฐํ™”

์—ฌ๋Ÿฌ ํ…Œ์ŠคํŠธ ๊ทธ๋ฃนํ™”

import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { describe } from "vitest";
import { UserModel } from "./user.model";

bootstrap(vi);

describe("์‚ฌ์šฉ์ž ์ƒ์„ฑ", () => {
  test("์ •์ƒ์ ์ธ ์ƒ์„ฑ", async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username: "john",
      email: "[email protected]",
      password: "password",
    });
    expect(user.id).toBeGreaterThan(0);
  });
  
  test("์ค‘๋ณต ์ด๋ฉ”์ผ ๊ฒ€์ฆ", async () => {
    const userModel = new UserModel();
    await userModel.create({
      username: "user1",
      email: "[email protected]",
      password: "password",
    });
    
    await expect(
      userModel.create({
        username: "user2",
        email: "[email protected]",
        password: "password",
      })
    ).rejects.toThrow();
  });
});

describe("์‚ฌ์šฉ์ž ์กฐํšŒ", () => {
  test("ID๋กœ ์กฐํšŒ", async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username: "test",
      email: "[email protected]",
      password: "password",
    });
    
    const { user: found } = await userModel.getUser("C", user.id);
    expect(found.id).toBe(user.id);
  });
  
  test("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‚ฌ์šฉ์ž ์กฐํšŒ", async () => {
    const userModel = new UserModel();
    await expect(
      userModel.getUser("C", 999999)
    ).rejects.toThrow();
  });
});

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

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

๊ฐ ํ…Œ์ŠคํŠธ๋Š” ๋…๋ฆฝ์ ์œผ๋กœ ์‹คํ–‰๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
// โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•: ๊ฐ ํ…Œ์ŠคํŠธ๊ฐ€ ๋…๋ฆฝ์ 
test("ํ…Œ์ŠคํŠธ 1", async () => {
  const userModel = new UserModel();
  const { user } = await userModel.create({ /* ... */ });
  // ํ…Œ์ŠคํŠธ ์ข…๋ฃŒ ํ›„ ์ž๋™ ๋กค๋ฐฑ
});

test("ํ…Œ์ŠคํŠธ 2", async () => {
  const userModel = new UserModel();
  // ๊นจ๋—ํ•œ DB ์ƒํƒœ์—์„œ ์‹œ์ž‘
  const { user } = await userModel.create({ /* ... */ });
});

// โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ•: ํ…Œ์ŠคํŠธ ๊ฐ„ ์˜์กด์„ฑ
let sharedUserId: number;

test("ํ…Œ์ŠคํŠธ 1", async () => {
  const userModel = new UserModel();
  const { user } = await userModel.create({ /* ... */ });
  sharedUserId = user.id; // โŒ ๋‹ค๋ฅธ ํ…Œ์ŠคํŠธ์™€ ๊ณต์œ 
});

test("ํ…Œ์ŠคํŠธ 2", async () => {
  const userModel = new UserModel();
  const { user } = await userModel.getUser("C", sharedUserId); // โŒ ์‹คํŒจํ•จ!
});

2. ๋ช…ํ™•ํ•œ ํ…Œ์ŠคํŠธ ์ด๋ฆ„

// โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•
test("์ค‘๋ณต ์ด๋ฉ”์ผ๋กœ ์‚ฌ์šฉ์ž ์ƒ์„ฑ ์‹œ ์—๋Ÿฌ ๋ฐœ์ƒ", async () => {
  // ...
});

// โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ•
test("ํ…Œ์ŠคํŠธ 1", async () => {
  // ...
});

3. Arrange-Act-Assert ํŒจํ„ด

test("๊ฒŒ์‹œ๊ธ€ ์ˆ˜์ •", async () => {
  // Arrange (์ค€๋น„)
  const postModel = new PostModel();
  const { post } = await postModel.create({
    title: "Original",
    content: "Content",
    author_id: 1,
  });
  
  // Act (์‹คํ–‰)
  await postModel.update(post.id, {
    title: "Updated",
  });
  
  // Assert (๊ฒ€์ฆ)
  const { post: updated } = await postModel.getPost("C", post.id);
  expect(updated.title).toBe("Updated");
});

์ฃผ์˜์‚ฌํ•ญ

ํ…Œ์ŠคํŠธ ์ž‘์„ฑ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ์ž๋™ ์ƒ์„ฑ ํŒŒ์ผ ์ˆ˜์ • ๊ธˆ์ง€: ์žฌ์ƒ์„ฑ ์‹œ ๋ฎ์–ด์”Œ์›Œ์ง‘๋‹ˆ๋‹ค
  2. ๋…๋ฆฝ์ ์ธ ํ…Œ์ŠคํŠธ: ํ…Œ์ŠคํŠธ ๊ฐ„ ์˜์กด์„ฑ ์—†์–ด์•ผ ํ•จ
  3. Transaction ๊ธฐ๋ฐ˜: ๊ฐ ํ…Œ์ŠคํŠธ ์ข…๋ฃŒ ํ›„ ์ž๋™ ๋กค๋ฐฑ
  4. ๋น„๋™๊ธฐ ํ•„์ˆ˜: ๋ชจ๋“  ํ…Œ์ŠคํŠธ๋Š” async ํ•จ์ˆ˜
  5. ๋ช…ํ™•ํ•œ ์ด๋ฆ„: ํ…Œ์ŠคํŠธ๊ฐ€ ๋ฌด์—‡์„ ๊ฒ€์ฆํ•˜๋Š”์ง€ ๋ช…ํ™•ํ•˜๊ฒŒ

๋‹ค์Œ ๋‹จ๊ณ„