> ## Documentation Index
> Fetch the complete documentation index at: https://sonamu.cartanova.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# 테스트 헬퍼

> Fixture 시스템과 테스트 유틸리티

Sonamu의 Fixture 시스템을 사용하여 테스트 데이터를 효율적으로 관리하는 방법을 알아봅니다.

## 테스트 헬퍼 개요

<CardGroup cols={2}>
  <Card title="Fixture Loader" icon="box">
    테스트 데이터 재사용 타입 안전한 로딩
  </Card>

  <Card title="Fixture Manager" icon="database">
    DB 간 데이터 복사 관계 자동 처리
  </Card>

  <Card title="간편한 설정" icon="wand-magic-sparkles">
    한 번 정의 여러 테스트 공유
  </Card>

  <Card title="자동 정리" icon="broom">
    Transaction 기반 자동 롤백
  </Card>
</CardGroup>

## Fixture Loader

### createFixtureLoader

테스트에서 사용할 fixture를 미리 정의하고 필요할 때 로드합니다.

```typescript theme={null}
// 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 params = {
      username: "john",
      email: "john@example.com",
      password: "password123",
    };
    const [id] = await UserModel.save([params]);
    return { id, ...params };
  },

  user02: async () => {
    const params = {
      username: "jane",
      email: "jane@example.com",
      password: "password456",
    };
    const [id] = await UserModel.save([params]);
    return { id, ...params };
  },

  admin: async () => {
    const params = {
      username: "admin",
      email: "admin@example.com",
      password: "admin123",
      role: "admin",
    };
    const [id] = await UserModel.save([params]);
    return { id, ...params };
  },

  // 게시글 fixtures
  post01: async () => {
    const params = {
      title: "First Post",
      content: "Content of first post",
      author_id: 1,
    };
    const [id] = await PostModel.save([params]);
    return { id, ...params };
  },
});
```

### 테스트에서 사용하기

```typescript theme={null}
// 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 { 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");
});
```

### 장점

**타입 안전성**:

```typescript theme={null}
// ✅ 타입 안전: IDE가 자동완성 제공
const { user01 } = await loadFixtures(["user01"]);
user01.username; // string

// ❌ 존재하지 않는 fixture
const { user999 } = await loadFixtures(["user999"]); // 타입 에러!
```

**재사용성**:

```typescript theme={null}
// 여러 테스트에서 동일한 fixture 사용
test("테스트 1", async () => {
  const { user01 } = await loadFixtures(["user01"]);
  // ...
});

test("테스트 2", async () => {
  const { user01 } = await loadFixtures(["user01"]);
  // ...
});
```

**선택적 로딩**:

```typescript theme={null}
// 필요한 것만 로드
const { user01, user02 } = await loadFixtures(["user01", "user02"]);

// admin은 로드하지 않음 → 생성 비용 절약
```

## 복잡한 Fixture 패턴

### 관계가 있는 Fixtures

```typescript theme={null}
// api/src/testing/fixture.ts
export const loadFixtures = createFixtureLoader({
  author: async () => {
    const params = {
      username: "author",
      email: "author@example.com",
      password: "password",
    };
    const [id] = await UserModel.save([params]);
    return { id, ...params };
  },

  authorPost: async () => {
    // author fixture 먼저 로드
    const { author } = await loadFixtures(["author"]);

    const params = {
      title: "Author's Post",
      content: "Content",
      author_id: author.id, // 관계 설정
    };
    const [id] = await PostModel.save([params]);
    return { id, ...params };
  },
});
```

```typescript theme={null}
// 테스트에서 사용
test("작성자와 게시글 관계", async () => {
  const { author, authorPost } = await loadFixtures(["author", "authorPost"]);

  expect(authorPost.author_id).toBe(author.id);
});
```

### 동적 Fixtures

```typescript theme={null}
// api/src/testing/fixture.ts
export const loadFixtures = createFixtureLoader({
  // 파라미터를 받는 fixture
  userWithEmail: async (email: string) => {
    const params = {
      username: email.split("@")[0],
      email,
      password: "password",
    };
    const [id] = await UserModel.save([params]);
    return { id, ...params };
  },

  // 여러 데이터 생성
  users10: async () => {
    const paramsList = Array.from({ length: 10 }, (_, i) => ({
      username: `user${i + 1}`,
      email: `user${i + 1}@example.com`,
      password: "password",
    }));
    const ids = await UserModel.save(paramsList);
    return paramsList.map((params, i) => ({ id: ids[i], ...params }));
  },
});
```

## Fixture Manager

### 개요

`FixtureManager`는 프로덕션 DB의 실제 데이터를 테스트 DB로 복사하는 기능을 제공합니다.

**주요 기능**:

* 프로덕션 → Fixture DB 데이터 추출
* Fixture DB → 테스트 DB 동기화
* 관계 자동 해결 (BelongsTo, HasMany, ManyToMany)
* 중복 데이터 처리

### DB sync

Fixture DB의 데이터를 테스트 DB로 동기화합니다.

```typescript theme={null}
// 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로 가져옵니다.

```typescript theme={null}
import { FixtureManager } from "sonamu/test";

// 초기화
FixtureManager.init();

// User ID 1, 2, 3을 Fixture DB로 가져오기
await FixtureManager.importFixture("User", [1, 2, 3]);

// 관련된 모든 데이터 자동 가져오기 (Posts, Comments 등)
```

**자동 관계 해결**:

```typescript theme={null}
// 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 패턴

```typescript theme={null}
// 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 params = {
      username: "guest",
      email: "guest@example.com",
      password: "password",
      role: "guest",
    };
    const [id] = await UserModel.save([params]);
    return { id, ...params };
  },

  // 2. 여러 데이터 (배열을 한 번에 전달)
  testUsers: async () => {
    const paramsList = [
      {
        username: "user1",
        email: "user1@example.com",
        password: "password",
      },
      {
        username: "user2",
        email: "user2@example.com",
        password: "password",
      },
    ];
    const ids = await UserModel.save(paramsList);
    return paramsList.map((params, i) => ({ id: ids[i], ...params }));
  },

  // 3. 관계가 있는 데이터
  userWithPosts: async () => {
    const userParams = {
      username: "blogger",
      email: "blogger@example.com",
      password: "password",
    };
    const [userId] = await UserModel.save([userParams]);
    const user = { id: userId, ...userParams };

    const postParamsList = [
      {
        title: "Post 1",
        content: "Content 1",
        author_id: user.id,
      },
      {
        title: "Post 2",
        content: "Content 2",
        author_id: user.id,
      },
    ];
    const postIds = await PostModel.save(postParamsList);
    const posts = postParamsList.map((params, i) => ({ id: postIds[i], ...params }));

    return { user, posts };
  },
});
```

### 복잡한 시나리오

```typescript theme={null}
// 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 [commentId] = await CommentModel.save([
    {
      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 이름 규칙

```typescript theme={null}
// ✅ 올바른 방법: 명확한 이름
export const loadFixtures = createFixtureLoader({
  adminUser: async () => {
    /* ... */
  },
  guestUser: async () => {
    /* ... */
  },
  publishedPost: async () => {
    /* ... */
  },
  draftPost: async () => {
    /* ... */
  },
});

// ❌ 잘못된 방법: 불명확한 이름
export const loadFixtures = createFixtureLoader({
  user1: async () => {
    /* ... */
  },
  user2: async () => {
    /* ... */
  },
  post1: async () => {
    /* ... */
  },
});
```

### 2. 최소한의 데이터

```typescript theme={null}
// ✅ 올바른 방법: 필요한 필드만
export const loadFixtures = createFixtureLoader({
  basicUser: async () => {
    const params = {
      username: "user",
      email: "user@example.com",
      password: "password",
    };
    const [id] = await UserModel.save([params]);
    return { id, ...params };
  },
});

// ❌ 잘못된 방법: 불필요한 필드까지
export const loadFixtures = createFixtureLoader({
  complexUser: async () => {
    const params = {
      username: "user",
      email: "user@example.com",
      password: "password",
      bio: "Long bio...",
      avatar: "https://...",
      preferences: {
        /* ... */
      },
      // ... 너무 많은 필드
    };
    const [id] = await UserModel.save([params]);
    return { id, ...params };
  },
});
```

### 3. Fixture 재사용

```typescript theme={null}
// ✅ 올바른 방법: 다른 fixture에서 재사용
export const loadFixtures = createFixtureLoader({
  user: async () => {
    const params = {
      username: "user",
      email: "user@example.com",
      password: "password",
    };
    const [id] = await UserModel.save([params]);
    return { id, ...params };
  },

  userPost: async () => {
    const { user } = await loadFixtures(["user"]);
    const params = {
      title: "Post",
      content: "Content",
      author_id: user.id,
    };
    const [id] = await PostModel.save([params]);
    return { id, ...params };
  },
});
```

## 에러 처리

Fixture 로딩 중 에러가 발생할 수 있습니다. 적절한 에러 처리로 테스트를 안정적으로 작성하세요.

### 기본 에러 처리

```typescript theme={null}
import { bootstrap, test } from "sonamu/test";
import { expect, vi } from "vitest";
import { loadFixtures } from "@/testing/fixture";

bootstrap(vi);

describe("PostModel with error handling", () => {
  test("fixture 로딩 실패 처리", async () => {
    // 잘못된 fixture 키 사용
    await expect(loadFixtures(["nonexistent"])).rejects.toThrow();

    // 올바른 에러 메시지 확인
    await expect(loadFixtures(["nonexistent"])).rejects.toThrow("Fixture 'nonexistent' not found");
  });

  test("DB 에러 처리", async () => {
    // DB 연결 실패 등의 에러 처리
    try {
      const { user } = await loadFixtures(["user01"]);
      // 테스트 로직
    } catch (error) {
      // 에러 로깅
      console.error("Fixture loading failed:", error);
      throw error; // 테스트 실패로 처리
    }
  });
});
```

### Fixture 생성 실패 처리

```typescript theme={null}
export const loadFixtures = createFixtureLoader({
  user01: async () => {
    try {
      const params = {
        username: "john",
        email: "john@example.com",
        password: "password123",
      };
      const [id] = await UserModel.save([params]);
      return { id, ...params };
    } catch (error) {
      // 에러 로깅 및 재전파
      console.error("Failed to create user01 fixture:", error);
      throw new Error(`user01 fixture creation failed: ${error.message}`);
    }
  },

  // 의존성이 있는 fixture의 에러 처리
  userPost: async () => {
    let user;
    try {
      ({ user } = await loadFixtures(["user"]));
    } catch (error) {
      throw new Error(`Failed to load user fixture: ${error.message}`);
    }

    try {
      const params = {
        title: "Post",
        content: "Content",
        author_id: user.id,
      };
      const [id] = await PostModel.save([params]);
      return { id, ...params };
    } catch (error) {
      throw new Error(`Failed to create post: ${error.message}`);
    }
  },
});
```

### 타임아웃 설정

Fixture 로딩이 오래 걸리는 경우 타임아웃을 설정할 수 있습니다.

```typescript theme={null}
import { describe, test, vi } from "vitest";

describe("PostModel", () => {
  test(
    "느린 fixture 로딩",
    async () => {
      // 5초 타임아웃 설정
      const { user } = await loadFixtures(["user01"]);
      // 테스트 로직
    },
    { timeout: 5000 },
  ); // Vitest 타임아웃 옵션

  test(
    "매우 느린 작업",
    async () => {
      const fixtures = await loadFixtures(["user01", "user02", "admin"]);
      // 많은 fixture 로딩
    },
    { timeout: 10000 },
  );
});
```

### 조건부 Fixture 로딩

환경에 따라 다른 fixture를 로드할 수 있습니다.

```typescript theme={null}
test("환경별 fixture", async () => {
  const env = process.env.NODE_ENV;

  let user;
  if (env === "test") {
    // 테스트 환경용 fixture
    ({ user } = await loadFixtures(["testUser"]));
  } else if (env === "development") {
    // 개발 환경용 fixture
    ({ user } = await loadFixtures(["devUser"]));
  } else {
    throw new Error("Unknown environment");
  }

  expect(user).toBeDefined();
  expect(user.email).toContain("@");
});
```

### 에러 복구 전략

```typescript theme={null}
export const loadFixtures = createFixtureLoader({
  resilientUser: async () => {
    // 재시도 로직
    const maxRetries = 3;
    let lastError;

    for (let i = 0; i < maxRetries; i++) {
      try {
        const params = {
          username: `user_${Date.now()}`, // 충돌 방지
          email: `user_${Date.now()}@example.com`,
          password: "password123",
        };
        const [id] = await UserModel.save([params]);
        return { id, ...params };
      } catch (error) {
        lastError = error;
        console.warn(`Retry ${i + 1}/${maxRetries} failed:`, error);

        // 짧은 대기 후 재시도
        await new Promise((resolve) => setTimeout(resolve, 100));
      }
    }

    // 모든 재시도 실패
    throw new Error(`Failed to create user after ${maxRetries} retries: ${lastError.message}`);
  },
});
```

<Info>
  **에러 처리 Best Practice**: - Fixture 생성 실패 시 명확한 에러 메시지 제공 - 의존성 있는
  fixture는 각 단계별로 에러 처리 - 타임아웃 설정으로 무한 대기 방지 - 재시도 로직은 신중하게 사용
  (DB 상태 일관성 고려) - 에러 로깅으로 디버깅 용이성 확보
</Info>

## 주의사항

<Warning>
  **Fixture 사용 시 주의사항**: 1. **Transaction 기반**: 각 테스트마다 자동 롤백됨 2. **최소
  데이터**: 테스트에 필요한 최소한의 데이터만 생성 3. **독립성**: 각 테스트는 독립적으로 실행되어야
  함 4. **타입 안전**: createFixtureLoader로 타입 안전성 보장 5. **명확한 이름**: fixture 이름은
  용도를 명확하게 표현 6. **에러 처리**: Fixture 로딩 실패에 대비한 에러 처리 필수
</Warning>

## 다음 단계

<CardGroup cols={2}>
  <Card title="테스트 구조" icon="sitemap" href="/ko/testing/writing-tests/test-structure">
    bootstrap, test, testAs
  </Card>

  <Card title="테스트 Scaffolding" icon="wand-magic-sparkles" href="/ko/testing/writing-tests/scaffolding-tests">
    자동 테스트 생성
  </Card>

  <Card title="Model System" icon="cube" href="/ko/core-concepts/model-system/overview">
    Model 이해하기
  </Card>

  <Card title="Vitest 공식 문서" icon="external-link" href="https://vitest.dev">
    Vitest 가이드
  </Card>
</CardGroup>
