메인 콘텐츠로 건너뛰기
pnpm fixture 명령어는 테스트에 사용할 일관된 데이터 세트를 관리합니다. 개발 환경의 실제 데이터를 테스트 환경으로 복사하여, 안정적이고 반복 가능한 테스트를 수행할 수 있습니다.

기본 개념

Fixture는 테스트용 고정된 데이터 세트입니다:
  • 일관성: 모든 테스트가 같은 초기 데이터로 시작
  • 격리: 테스트 DB와 개발 DB 분리
  • 재현성: 같은 조건에서 반복 테스트 가능
  • 관계 보존: 외래키 관계가 유지됨

명령어

init - 테스트 DB 초기화

개발 DB의 스키마를 테스트 DB로 복사합니다.
pnpm fixture init
실행 과정:
DUMP...
  ✓ Schema dumped from development_master

SYNC to (REMOTE) Fixture DB...
  ✓ Database myapp_fixture created
  ✓ Schema applied

SYNC to (LOCAL) Testing DB...
  ✓ Database myapp_test created
  ✓ Schema applied

Fixture initialization completed!
생성되는 데이터베이스:
  • Fixture DB (원격): 공유 fixture 저장소
  • Test DB (로컬): 로컬 테스트용 DB
init스키마만 복사합니다. 데이터는 포함되지 않습니다.

import - 데이터 가져오기

개발 DB에서 특정 레코드를 fixture로 저장합니다.
pnpm fixture import
대화형 프롬프트가 표시됩니다:
? Please select entity: (Use arrow keys)
❯ User
  Post
  Comment
  
? Enter record IDs (comma-separated): 1,2,3

Importing fixtures...
  ✓ Imported User #1
  ✓ Imported User #2
  ✓ Imported User #3
  ✓ Imported related records (5 dependencies)

Fixture import completed!
자동으로 포함되는 관련 데이터:
  • 외래키로 참조되는 레코드
  • 관계 테이블의 레코드
  • 역참조 레코드 (선택 가능)

sync - 동기화

저장된 fixture를 테스트 DB에 적용합니다.
pnpm fixture sync
실행 과정:
Syncing fixtures...

Clearing test database...
  ✓ Cleared all tables

Applying fixtures...
  ✓ Applied 3 users
  ✓ Applied 7 posts
  ✓ Applied 12 comments
  ✓ Applied 5 categories

Fixture sync completed!
테스트 실행 전에 sync를 호출하면 항상 깨끗한 상태에서 시작할 수 있습니다.

사용 워크플로우

1. 초기 설정

프로젝트 시작 시 한 번만 실행합니다.
# 테스트 DB 생성 및 스키마 복사
pnpm fixture init

2. 필요한 데이터 선택

테스트에 필요한 실제 데이터를 가져옵니다.
# User #1, #2, #3 가져오기
pnpm fixture import

# Entity: User
# IDs: 1,2,3

3. 테스트 작성

user.model.test.ts
import { FixtureManager } from "sonamu/test";

beforeAll(async () => {
  // Fixture 동기화
  await FixtureManager.sync();
});

test("User를 이메일로 찾기", async () => {
  // Fixture의 User #1 사용
  const user = await UserModel.findByEmail("[email protected]");
  
  expect(user).toBeDefined();
  expect(user.id).toBe(1);
});

4. 테스트 실행

pnpm test
테스트가 실행될 때마다 fixture가 자동으로 동기화됩니다.

Fixture 파일

저장 위치

📁src/
📁fixtures/
📄JSONusers.json - User fixture
📄JSONposts.json - Post fixture
📄JSONcomments.json - Comment fixture

파일 형식

src/fixtures/users.json
[
  {
    "id": 1,
    "email": "[email protected]",
    "name": "홍길동",
    "created_at": "2024-01-15T00:00:00.000Z"
  },
  {
    "id": 2,
    "email": "[email protected]",
    "name": "김철수",
    "created_at": "2024-01-16T00:00:00.000Z"
  }
]
특징:
  • JSON 형식
  • Entity별로 파일 분리
  • 관계 데이터 자동 포함
  • Git으로 버전 관리

관계 데이터 처리

외래키 자동 포함

Post를 import하면 연결된 User도 자동으로 포함됩니다.
pnpm fixture import

# Entity: Post
# IDs: 1

# 자동으로 포함됨:
# ✓ Post #1
# ✓ User #5 (author)
# ✓ Category #2 (category)

N:M 관계 처리

다대다 관계 테이블도 자동으로 포함됩니다.
pnpm fixture import

# Entity: Post
# IDs: 1

# 자동으로 포함됨:
# ✓ Post #1
# ✓ post_tags (중간 테이블)
# ✓ Tag #1, #2, #3 (연결된 태그들)

테스트 격리

각 테스트마다 초기화

describe("User CRUD", () => {
  beforeEach(async () => {
    // 각 테스트 전에 fixture 재적용
    await FixtureManager.sync();
  });

  test("사용자 생성", async () => {
    const user = await UserModel.create({
      email: "[email protected]",
      name: "신규사용자",
    });
    
    expect(user.id).toBeDefined();
  });

  test("사용자 삭제", async () => {
    await UserModel.deleteById(1);
    
    const user = await UserModel.findById(1);
    expect(user).toBeNull();
  });
});
격리 효과:
  • 각 테스트가 독립적
  • 테스트 순서 무관
  • 병렬 실행 가능

트랜잭션 격리

import { Sonamu } from "sonamu";

test("트랜잭션 테스트", async () => {
  await Sonamu.runScript(async () => {
    // 트랜잭션 내에서 실행
    const user = await UserModel.create({...});
    const post = await PostModel.create({...});
    
    // 테스트 완료 후 자동 롤백
  });
});

실전 예제

복잡한 관계 테스트

describe("Post with Comments", () => {
  beforeAll(async () => {
    // Post #1 (+ User #1, Comments)
    await FixtureManager.sync();
  });

  test("댓글이 있는 Post 조회", async () => {
    const post = await PostModel.findById(1, {
      include: ["comments"],
    });
    
    expect(post.comments).toHaveLength(3);
    expect(post.comments[0].author_id).toBe(2);
  });
});

특정 시나리오 데이터

describe("Premium User Features", () => {
  beforeAll(async () => {
    // Premium user만 import
    await FixtureManager.importFixture("User", [10, 11, 12]);
    await FixtureManager.sync();
  });

  test("프리미엄 기능 접근", async () => {
    const user = await UserModel.findById(10);
    
    expect(user.isPremium).toBe(true);
    expect(await user.canAccessFeature("advanced")).toBe(true);
  });
});

문제 해결

Fixture DB 연결 실패

문제: 원격 fixture DB에 접근 불가
Error: connect ECONNREFUSED
해결:
sonamu.config.ts
export default {
  database: {
    fixture: {
      client: "pg",
      connection: {
        host: "fixture-db.example.com",  // 올바른 호스트
        database: "myapp_fixture",
        user: "postgres",
        password: process.env.FIXTURE_DB_PASSWORD,
      },
    },
  },
};

관계 데이터 누락

문제: 외래키 에러 발생
Error: Foreign key constraint violation
해결:
# 관련 데이터를 먼저 import
pnpm fixture import
# Entity: User
# IDs: 1,2,3

pnpm fixture import
# Entity: Post
# IDs: 1,2,3  # User 1,2,3을 참조

Fixture 충돌

문제: ID 중복
Error: Duplicate entry '1' for key 'PRIMARY'
해결:
# Test DB 초기화
pnpm fixture init

# Fixture 재동기화
pnpm fixture sync

베스트 프랙티스

1. 최소한의 데이터

# ❌ 모든 데이터 import
pnpm fixture import
# IDs: 1-1000

# ✅ 필요한 데이터만
pnpm fixture import
# IDs: 1,2,3  # 대표 케이스만

2. 의미있는 데이터

// ✅ 테스트 목적이 명확한 데이터
{
  "id": 1,
  "email": "[email protected]",
  "role": "admin",
  "name": "관리자"
}
{
  "id": 2,
  "email": "[email protected]",
  "role": "user",
  "name": "일반사용자"
}

3. 버전 관리

# Fixture 파일을 Git에 포함
git add src/fixtures/
git commit -m "Update test fixtures"

4. CI/CD 통합

.github/workflows/test.yml
jobs:
  test:
    steps:
      - name: Setup Database
        run: |
          pnpm fixture init
          pnpm fixture sync
      
      - name: Run Tests
        run: pnpm test

다음 단계