pnpm generate 명령어로 모델 테스트 파일을 자동 생성합니다.
Scaffolding 개요
자동 생성
model.test.ts 생성기본 구조 포함
bootstrap 포함
테스트 환경 자동 설정즉시 실행 가능
CRUD 테스트
기본 테스트 스켈레톤커스터마이징 가능
타입 안전
모델과 연동자동완성 지원
테스트 파일 생성
명령어
복사
pnpm generate
{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");
});
주의사항
테스트 작성 시 주의사항:
- 자동 생성 파일 수정 금지: 재생성 시 덮어씌워집니다
- 독립적인 테스트: 테스트 간 의존성 없어야 함
- Transaction 기반: 각 테스트 종료 후 자동 롤백
- 비동기 필수: 모든 테스트는 async 함수
- 명확한 이름: 테스트가 무엇을 검증하는지 명확하게
