๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Sonamu์˜ createFixtureLoader๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ…Œ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ์ •์˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.

Fixture ์ƒ์„ฑ ๊ฐœ์š”

createFixtureLoader

ํƒ€์ž… ์•ˆ์ „ํ•œ ์ •์˜์ž๋™์™„์„ฑ ์ง€์›

์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅ

ํ•œ ๋ฒˆ ์ •์˜์—ฌ๋Ÿฌ ํ…Œ์ŠคํŠธ ๊ณต์œ 

๊ด€๊ณ„ ์ฒ˜๋ฆฌ

BelongsTo ์ž๋™ ํ•ด๊ฒฐ์ค‘์ฒฉ๋œ ๋ฐ์ดํ„ฐ

์„ ํƒ์  ๋กœ๋”ฉ

ํ•„์š”ํ•œ ๊ฒƒ๋งŒ๋น ๋ฅธ ํ…Œ์ŠคํŠธ

createFixtureLoader

๊ธฐ๋ณธ ๊ตฌ์กฐ

// 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({
  // Fixture ์ด๋ฆ„: ๋กœ๋” ํ•จ์ˆ˜
  user01: async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username: "john",
      email: "[email protected]",
      password: "password123",
    });
    return user;
  },
  
  admin: async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username: "admin",
      email: "[email protected]",
      password: "admin123",
      role: "admin",
    });
    return user;
  },
});

ํƒ€์ž… ์ •์˜

/**
 * Fixture Loader Factory
 *
 * ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉํ•  fixture๋ฅผ ๋กœ๋“œํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑ
 */
export function createFixtureLoader<T extends Record<string, () => Promise<unknown>>>(
  loaders: T
) {
  return async function loadFixtures<K extends keyof T>(
    names: K[],
  ): Promise<{ [P in K]: Awaited<ReturnType<T[P]>> }> {
    return Object.fromEntries(
      await Promise.all(
        names.map(async (name) => [name, await loaders[name]()])
      ),
    );
  };
}
ํŠน์ง•:
  • ํƒ€์ž… ์•ˆ์ „: TypeScript๊ฐ€ fixture ์ด๋ฆ„๊ณผ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ์ถ”๋ก 
  • ๋ณ‘๋ ฌ ๋กœ๋”ฉ: Promise.all๋กœ ์—ฌ๋Ÿฌ fixture ๋™์‹œ ๋กœ๋“œ
  • ์„ ํƒ์ : ํ•„์š”ํ•œ fixture๋งŒ ๋กœ๋“œ

์‹ค์ „ ์˜ˆ์ œ

๋‹จ์ˆœ Fixtures

// api/src/testing/fixture.ts
import { createFixtureLoader } from "sonamu/test";
import { UserModel } from "@/models/user.model";

export const loadFixtures = createFixtureLoader({
  // ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž
  guestUser: async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username: "guest",
      email: "[email protected]",
      password: "password",
      role: "guest",
    });
    return user;
  },
  
  // ํ”„๋ฆฌ๋ฏธ์—„ ์‚ฌ์šฉ์ž
  premiumUser: async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username: "premium",
      email: "[email protected]",
      password: "password",
      role: "premium",
      subscription_expires_at: new Date("2025-12-31"),
    });
    return user;
  },
  
  // ๊ด€๋ฆฌ์ž
  adminUser: async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username: "admin",
      email: "[email protected]",
      password: "password",
      role: "admin",
    });
    return user;
  },
});

๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” Fixtures

// api/src/testing/fixture.ts
import { createFixtureLoader } from "sonamu/test";
import { UserModel } from "@/models/user.model";
import { PostModel } from "@/models/post.model";
import { CommentModel } from "@/models/comment.model";

export const loadFixtures = createFixtureLoader({
  // ๊ธฐ๋ณธ ์‚ฌ์šฉ์ž
  author: async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username: "author",
      email: "[email protected]",
      password: "password",
    });
    return user;
  },
  
  // ์ž‘์„ฑ์ž์˜ ๊ฒŒ์‹œ๊ธ€ (author fixture ์žฌ์‚ฌ์šฉ)
  authorPost: async () => {
    // ๋‹ค๋ฅธ fixture ๋กœ๋“œ
    const { author } = await loadFixtures(["author"]);
    
    const postModel = new PostModel();
    const { post } = await postModel.create({
      title: "Author's First Post",
      content: "This is the content",
      author_id: author.id,  // ๊ด€๊ณ„ ์„ค์ •
    });
    return post;
  },
  
  // ๊ฒŒ์‹œ๊ธ€์˜ ๋Œ“๊ธ€ (authorPost fixture ์žฌ์‚ฌ์šฉ)
  postComment: async () => {
    const { author, authorPost } = await loadFixtures(["author", "authorPost"]);
    
    const commentModel = new CommentModel();
    const { comment } = await commentModel.create({
      content: "Great post!",
      post_id: authorPost.id,
      author_id: author.id,
    });
    return comment;
  },
});

์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ

export const loadFixtures = createFixtureLoader({
  // 10๋ช…์˜ ์‚ฌ์šฉ์ž
  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;
  },
  
  // ์‚ฌ์šฉ์ž์™€ ๊ทธ์˜ ๊ฒŒ์‹œ๊ธ€๋“ค
  userWith5Posts: async () => {
    const userModel = new UserModel();
    const postModel = new PostModel();
    
    const { user } = await userModel.create({
      username: "blogger",
      email: "[email protected]",
      password: "password",
    });
    
    const posts = [];
    for (let i = 1; i <= 5; i++) {
      const { post } = await postModel.create({
        title: `Post ${i}`,
        content: `Content ${i}`,
        author_id: user.id,
      });
      posts.push(post);
    }
    
    return { user, posts };
  },
});

๋ณต์žกํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค

export const loadFixtures = createFixtureLoader({
  // ์™„์ „ํ•œ ๋ธ”๋กœ๊ทธ ์‹œ๋‚˜๋ฆฌ์˜ค
  blogScenario: async () => {
    const userModel = new UserModel();
    const postModel = new PostModel();
    const commentModel = new CommentModel();
    
    // 1. ์ž‘์„ฑ์ž ์ƒ์„ฑ
    const { user: author } = await userModel.create({
      username: "author",
      email: "[email protected]",
      password: "password",
    });
    
    // 2. ๋…์ž๋“ค ์ƒ์„ฑ
    const readers = await Promise.all([
      userModel.create({
        username: "reader1",
        email: "[email protected]",
        password: "password",
      }),
      userModel.create({
        username: "reader2",
        email: "[email protected]",
        password: "password",
      }),
    ]);
    
    // 3. ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ฑ
    const { post } = await postModel.create({
      title: "Popular Post",
      content: "This post has many comments",
      author_id: author.id,
    });
    
    // 4. ๋Œ“๊ธ€ ์ƒ์„ฑ
    const comments = await Promise.all([
      commentModel.create({
        content: "First comment",
        post_id: post.id,
        author_id: readers[0].user.id,
      }),
      commentModel.create({
        content: "Second comment",
        post_id: post.id,
        author_id: readers[1].user.id,
      }),
      commentModel.create({
        content: "Author reply",
        post_id: post.id,
        author_id: author.id,
      }),
    ]);
    
    return {
      author,
      readers: readers.map(r => r.user),
      post,
      comments: comments.map(c => c.comment),
    };
  },
});

ํ…Œ์ŠคํŠธ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

๊ธฐ๋ณธ ์‚ฌ์šฉ

// 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 { author, authorPost } = await loadFixtures(["author", "authorPost"]);
  
  // ํ…Œ์ŠคํŠธ
  const postModel = new PostModel();
  const { posts } = await postModel.getPostsByAuthor(author.id);
  
  expect(posts).toHaveLength(1);
  expect(posts[0].id).toBe(authorPost.id);
});

ํƒ€์ž… ์•ˆ์ „์„ฑ

test("ํƒ€์ž… ์•ˆ์ „ํ•œ fixture ์‚ฌ์šฉ", async () => {
  // โœ… IDE๊ฐ€ fixture ์ด๋ฆ„ ์ž๋™์™„์„ฑ ์ œ๊ณต
  const { author, authorPost } = await loadFixtures(["author", "authorPost"]);
  
  // โœ… ํƒ€์ž… ์ถ”๋ก 
  author.username;  // string
  authorPost.title; // string
  
  // โŒ ์กด์žฌํ•˜์ง€ ์•Š๋Š” fixture
  const { invalid } = await loadFixtures(["invalid"]); // ํƒ€์ž… ์—๋Ÿฌ!
});

์„ ํƒ์  ๋กœ๋”ฉ

test("ํ•„์š”ํ•œ fixture๋งŒ ๋กœ๋“œ", async () => {
  // ํ•„์š”ํ•œ ๊ฒƒ๋งŒ
  const { author } = await loadFixtures(["author"]);
  
  // authorPost๋Š” ๋กœ๋“œํ•˜์ง€ ์•Š์Œ โ†’ ์ƒ์„ฑ ๋น„์šฉ ์ ˆ์•ฝ
});

test("์—ฌ๋Ÿฌ fixture ๋™์‹œ ๋กœ๋“œ", async () => {
  // ๋ณ‘๋ ฌ๋กœ ๋กœ๋“œ (Promise.all)
  const { author, premiumUser, adminUser } = await loadFixtures([
    "author",
    "premiumUser",
    "adminUser",
  ]);
  
  expect(author.role).toBe("user");
  expect(premiumUser.role).toBe("premium");
  expect(adminUser.role).toBe("admin");
});

๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

1. ๋ช…ํ™•ํ•œ ์ด๋ฆ„

// โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•: ์šฉ๋„๊ฐ€ ๋ช…ํ™•ํ•œ ์ด๋ฆ„
export const loadFixtures = createFixtureLoader({
  adminUser: async () => { /* ... */ },
  guestUser: async () => { /* ... */ },
  publishedPost: async () => { /* ... */ },
  draftPost: async () => { /* ... */ },
  premiumSubscription: async () => { /* ... */ },
});

// โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ•: ๋ถˆ๋ช…ํ™•ํ•œ ์ด๋ฆ„
export const loadFixtures = createFixtureLoader({
  user1: async () => { /* ... */ },
  user2: async () => { /* ... */ },
  post1: async () => { /* ... */ },
  thing: 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: "Very 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 () => {
    // user fixture ์žฌ์‚ฌ์šฉ
    const { user } = await loadFixtures(["user"]);
    
    const postModel = new PostModel();
    const { post } = await postModel.create({
      title: "Post",
      content: "Content",
      author_id: user.id,
    });
    return post;
  },
  
  userPostComment: async () => {
    // userPost fixture ์žฌ์‚ฌ์šฉ (user๋„ ์ž๋™์œผ๋กœ ํฌํ•จ๋จ)
    const { user, userPost } = await loadFixtures(["user", "userPost"]);
    
    const commentModel = new CommentModel();
    const { comment } = await commentModel.create({
      content: "Comment",
      post_id: userPost.id,
      author_id: user.id,
    });
    return comment;
  },
});

4. ์ผ๊ด€๋œ ๊ตฌ์กฐ

// โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•: ์ผ๊ด€๋œ ๋„ค์ด๋ฐ๊ณผ ๊ตฌ์กฐ
export const loadFixtures = createFixtureLoader({
  // ์‚ฌ์šฉ์ž ๊ด€๋ จ
  adminUser: async () => { /* ... */ },
  guestUser: async () => { /* ... */ },
  premiumUser: async () => { /* ... */ },
  
  // ๊ฒŒ์‹œ๊ธ€ ๊ด€๋ จ
  publishedPost: async () => { /* ... */ },
  draftPost: async () => { /* ... */ },
  
  // ์‹œ๋‚˜๋ฆฌ์˜ค
  blogScenario: async () => { /* ... */ },
  forumScenario: async () => { /* ... */ },
});

๊ณ ๊ธ‰ ํŒจํ„ด

ํŒฉํ† ๋ฆฌ ํ•จ์ˆ˜

// ๋™์ ์œผ๋กœ fixture ์ƒ์„ฑ
function createUserFixture(username: string, role: string) {
  return async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username,
      email: `${username}@example.com`,
      password: "password",
      role,
    });
    return user;
  };
}

export const loadFixtures = createFixtureLoader({
  adminUser: createUserFixture("admin", "admin"),
  guestUser: createUserFixture("guest", "guest"),
  moderatorUser: createUserFixture("moderator", "moderator"),
});

์ฃผ์˜์‚ฌํ•ญ

Fixture ์ž‘์„ฑ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. Transaction ๊ธฐ๋ฐ˜: ๊ฐ ํ…Œ์ŠคํŠธ๋งˆ๋‹ค ์ž๋™ ๋กค๋ฐฑ๋จ
  2. ์ตœ์†Œ ๋ฐ์ดํ„ฐ: ํ…Œ์ŠคํŠธ์— ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ๋ฐ์ดํ„ฐ๋งŒ ์ƒ์„ฑ
  3. ๋…๋ฆฝ์„ฑ: Fixture๋Š” ์„œ๋กœ ๋…๋ฆฝ์ ์ด์–ด์•ผ ํ•จ
  4. ์žฌ์‚ฌ์šฉ: ๊ณตํ†ต fixture๋ฅผ ์ ๊ทน ํ™œ์šฉ
  5. ๋ช…ํ™•ํ•œ ์ด๋ฆ„: fixture ์ด๋ฆ„์€ ์šฉ๋„๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ํ‘œํ˜„

๋‹ค์Œ ๋‹จ๊ณ„