메인 콘텐츠로 건너뛰기
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. 명확한 이름: 테스트가 무엇을 검증하는지 명확하게

다음 단계