메인 콘텐츠로 건너뛰기
Sonamu의 Fixture 시스템을 사용하여 테스트 데이터를 효율적으로 관리하는 방법을 알아봅니다.

테스트 헬퍼 개요

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();
실행 과정:
  1. 테스트 DB 초기화 (기존 데이터 삭제)
  2. Fixture DB 덤프 (pg_dump)
  3. 테스트 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 사용 시 주의사항:
  1. Transaction 기반: 각 테스트마다 자동 롤백됨
  2. 최소 데이터: 테스트에 필요한 최소한의 데이터만 생성
  3. 독립성: 각 테스트는 독립적으로 실행되어야 함
  4. 타입 안전: createFixtureLoader로 타입 안전성 보장
  5. 명확한 이름: fixture 이름은 용도를 명확하게 표현

다음 단계