테스트 헬퍼 개요
Fixture Loader
테스트 데이터 재사용타입 안전한 로딩
Fixture Manager
DB 간 데이터 복사관계 자동 처리
간편한 설정
한 번 정의여러 테스트 공유
자동 정리
Transaction 기반자동 롤백
Fixture Loader
createFixtureLoader
테스트에서 사용할 fixture를 미리 정의하고 필요할 때 로드합니다.복사
// api/src/testing/fixture.ts
import { createFixtureLoader } from "sonamu/test";
import { UserModel } from "@/models/user.model";
import { PostModel } from "@/models/post.model";
export const loadFixtures = createFixtureLoader({
// 사용자 fixtures
user01: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "john",
email: "[email protected]",
password: "password123",
});
return user;
},
user02: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "jane",
email: "[email protected]",
password: "password456",
});
return user;
},
admin: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "admin",
email: "[email protected]",
password: "admin123",
role: "admin",
});
return user;
},
// 게시글 fixtures
post01: async () => {
const postModel = new PostModel();
const { post } = await postModel.create({
title: "First Post",
content: "Content of first post",
author_id: 1,
});
return post;
},
});
테스트에서 사용하기
복사
// api/src/models/post.model.test.ts
import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { PostModel } from "./post.model";
import { loadFixtures } from "@/testing/fixture";
bootstrap(vi);
test("사용자의 게시글 조회", async () => {
// Fixtures 로드
const { user01, post01 } = await loadFixtures(["user01", "post01"]);
// 테스트
const postModel = new PostModel();
const { posts } = await postModel.getPostsByUser(user01.id);
expect(posts).toHaveLength(1);
expect(posts[0].id).toBe(post01.id);
});
test("관리자 권한 테스트", async () => {
// 필요한 fixture만 로드
const { admin } = await loadFixtures(["admin"]);
expect(admin.role).toBe("admin");
});
장점
타입 안전성:복사
// ✅ 타입 안전: IDE가 자동완성 제공
const { user01 } = await loadFixtures(["user01"]);
user01.username; // string
// ❌ 존재하지 않는 fixture
const { user999 } = await loadFixtures(["user999"]); // 타입 에러!
복사
// 여러 테스트에서 동일한 fixture 사용
test("테스트 1", async () => {
const { user01 } = await loadFixtures(["user01"]);
// ...
});
test("테스트 2", async () => {
const { user01 } = await loadFixtures(["user01"]);
// ...
});
복사
// 필요한 것만 로드
const { user01, user02 } = await loadFixtures(["user01", "user02"]);
// admin은 로드하지 않음 → 생성 비용 절약
복잡한 Fixture 패턴
관계가 있는 Fixtures
복사
// api/src/testing/fixture.ts
export const loadFixtures = createFixtureLoader({
author: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "author",
email: "[email protected]",
password: "password",
});
return user;
},
authorPost: async () => {
// author fixture 먼저 로드
const { author } = await loadFixtures(["author"]);
const postModel = new PostModel();
const { post } = await postModel.create({
title: "Author's Post",
content: "Content",
author_id: author.id, // 관계 설정
});
return post;
},
});
복사
// 테스트에서 사용
test("작성자와 게시글 관계", async () => {
const { author, authorPost } = await loadFixtures(["author", "authorPost"]);
expect(authorPost.author_id).toBe(author.id);
});
동적 Fixtures
복사
// api/src/testing/fixture.ts
export const loadFixtures = createFixtureLoader({
// 파라미터를 받는 fixture
userWithEmail: async (email: string) => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: email.split("@")[0],
email,
password: "password",
});
return user;
},
// 여러 데이터 생성
users10: async () => {
const userModel = new UserModel();
const users = [];
for (let i = 1; i <= 10; i++) {
const { user } = await userModel.create({
username: `user${i}`,
email: `user${i}@example.com`,
password: "password",
});
users.push(user);
}
return users;
},
});
Fixture Manager
개요
FixtureManager는 프로덕션 DB의 실제 데이터를 테스트 DB로 복사하는 기능을 제공합니다.
주요 기능:
- 프로덕션 → Fixture DB 데이터 추출
- Fixture DB → 테스트 DB 동기화
- 관계 자동 해결 (BelongsTo, HasMany, ManyToMany)
- 중복 데이터 처리
DB sync
Fixture DB의 데이터를 테스트 DB로 동기화합니다.복사
// api/src/application/sonamu.ts
import { FixtureManager } from "sonamu/test";
// Fixture DB → Test DB 동기화
await FixtureManager.sync();
- 테스트 DB 초기화 (기존 데이터 삭제)
- Fixture DB 덤프 (
pg_dump) - 테스트 DB 복원 (
pg_restore)
importFixture
프로덕션 DB의 특정 데이터를 Fixture DB로 가져옵니다.복사
import { FixtureManager } from "sonamu/test";
// 초기화
FixtureManager.init();
// User ID 1, 2, 3을 Fixture DB로 가져오기
await FixtureManager.importFixture("User", [1, 2, 3]);
// 관련된 모든 데이터 자동 가져오기 (Posts, Comments 등)
복사
// User ID 1만 요청했지만...
await FixtureManager.importFixture("User", [1]);
// 자동으로 가져옴:
// - User #1
// - User #1의 Profile (OneToOne)
// - User #1의 Posts (HasMany)
// - Post의 Comments (HasMany)
// - Comment의 Author (BelongsTo)
실전 예제
기본 Fixture 패턴
복사
// api/src/testing/fixture.ts
import { createFixtureLoader } from "sonamu/test";
import { UserModel } from "@/models/user.model";
import { PostModel } from "@/models/post.model";
export const loadFixtures = createFixtureLoader({
// 1. 단순 데이터
guestUser: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "guest",
email: "[email protected]",
password: "password",
role: "guest",
});
return user;
},
// 2. 여러 데이터
testUsers: async () => {
const userModel = new UserModel();
const users = await Promise.all([
userModel.create({
username: "user1",
email: "[email protected]",
password: "password",
}),
userModel.create({
username: "user2",
email: "[email protected]",
password: "password",
}),
]);
return users.map(({ user }) => user);
},
// 3. 관계가 있는 데이터
userWithPosts: async () => {
const userModel = new UserModel();
const postModel = new PostModel();
const { user } = await userModel.create({
username: "blogger",
email: "[email protected]",
password: "password",
});
const posts = await Promise.all([
postModel.create({
title: "Post 1",
content: "Content 1",
author_id: user.id,
}),
postModel.create({
title: "Post 2",
content: "Content 2",
author_id: user.id,
}),
]);
return {
user,
posts: posts.map(({ post }) => post),
};
},
});
복잡한 시나리오
복사
// api/src/models/comment.model.test.ts
import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { CommentModel } from "./comment.model";
import { loadFixtures } from "@/testing/fixture";
bootstrap(vi);
test("댓글 작성 및 조회", async () => {
// 필요한 fixtures 로드
const { userWithPosts } = await loadFixtures(["userWithPosts"]);
const { user, posts } = userWithPosts;
const commentModel = new CommentModel();
// 댓글 작성
const { comment } = await commentModel.create({
content: "Great post!",
post_id: posts[0].id,
author_id: user.id,
});
// 댓글 조회
const { comments } = await commentModel.getCommentsByPost(posts[0].id);
expect(comments).toHaveLength(1);
expect(comments[0].content).toBe("Great post!");
expect(comments[0].author_id).toBe(user.id);
});
베스트 프랙티스
1. Fixture 이름 규칙
복사
// ✅ 올바른 방법: 명확한 이름
export const loadFixtures = createFixtureLoader({
adminUser: async () => { /* ... */ },
guestUser: async () => { /* ... */ },
publishedPost: async () => { /* ... */ },
draftPost: async () => { /* ... */ },
});
// ❌ 잘못된 방법: 불명확한 이름
export const loadFixtures = createFixtureLoader({
user1: async () => { /* ... */ },
user2: async () => { /* ... */ },
post1: async () => { /* ... */ },
});
2. 최소한의 데이터
복사
// ✅ 올바른 방법: 필요한 필드만
export const loadFixtures = createFixtureLoader({
basicUser: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "user",
email: "[email protected]",
password: "password",
});
return user;
},
});
// ❌ 잘못된 방법: 불필요한 필드까지
export const loadFixtures = createFixtureLoader({
complexUser: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "user",
email: "[email protected]",
password: "password",
bio: "Long bio...",
avatar: "https://...",
preferences: { /* ... */ },
// ... 너무 많은 필드
});
return user;
},
});
3. Fixture 재사용
복사
// ✅ 올바른 방법: 다른 fixture에서 재사용
export const loadFixtures = createFixtureLoader({
user: async () => {
const userModel = new UserModel();
const { user } = await userModel.create({
username: "user",
email: "[email protected]",
password: "password",
});
return user;
},
userPost: async () => {
const { user } = await loadFixtures(["user"]);
const postModel = new PostModel();
const { post } = await postModel.create({
title: "Post",
content: "Content",
author_id: user.id,
});
return post;
},
});
주의사항
Fixture 사용 시 주의사항:
- Transaction 기반: 각 테스트마다 자동 롤백됨
- 최소 데이터: 테스트에 필요한 최소한의 데이터만 생성
- 독립성: 각 테스트는 독립적으로 실행되어야 함
- 타입 안전: createFixtureLoader로 타입 안전성 보장
- 명확한 이름: fixture 이름은 용도를 명확하게 표현
