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

Fixture ๋กœ๋”ฉ ๊ฐœ์š”

์‹ค์ œ ๋ฐ์ดํ„ฐ

ํ”„๋กœ๋•์…˜ ๋ฐ์ดํ„ฐ ํ˜„์‹ค์ ์ธ ํ…Œ์ŠคํŠธ

๊ด€๊ณ„ ์ž๋™ ํ•ด๊ฒฐ

BelongsTo ์ž๋™ ์ถ”์  ์—ฐ๊ฒฐ๋œ ๋ฐ์ดํ„ฐ ํ•จ๊ป˜

์ค‘๋ณต ๋ฐฉ์ง€

ID ๊ธฐ๋ฐ˜ ๊ด€๋ฆฌ ์•ˆ์ „ํ•œ ๊ฐ€์ ธ์˜ค๊ธฐ

๊ฐ„ํŽธํ•œ ๋ช…๋ น์–ด

ํ•œ ์ค„๋กœ ์‹คํ–‰ ์ž๋™ ๋™๊ธฐํ™”

pnpm sonamu fixture import

ํ”„๋กœ๋•์…˜ DB์˜ ํŠน์ • ๋ ˆ์ฝ”๋“œ๋ฅผ Fixture DB๋กœ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.

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

pnpm sonamu fixture import [entityId] [recordIds]
ํŒŒ๋ผ๋ฏธํ„ฐ:
  • entityId: Entity ์ด๋ฆ„ (์˜ˆ: โ€œUserโ€, โ€œPostโ€)
  • recordIds: ๊ฐ€์ ธ์˜ฌ ๋ ˆ์ฝ”๋“œ ID ๋ฐฐ์—ด (์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„)

์˜ˆ์ œ

# User ID 1, 2, 3์„ Fixture DB๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ
pnpm sonamu fixture import User 1,2,3

# Post ID 42๋ฅผ Fixture DB๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ
pnpm sonamu fixture import Post 42

์ž‘๋™ ์›๋ฆฌ

1. ๋ ˆ์ฝ”๋“œ ์ถ”์ถœ

// sonamu/src/testing/fixture-manager.ts
async function importFixture(entityId: string, ids: number[]) {
  const queries = await Promise.all(
    ids.map(async (id) => {
      // ๊ฐ ID์— ๋Œ€ํ•œ import ์ฟผ๋ฆฌ ์ƒ์„ฑ
      return await this.getImportQueries(entityId, "id", id);
    })
  );

  // ์ฟผ๋ฆฌ ์‹คํ–‰
  const wdb = BaseModel.getDB("w");
  for (const query of queries) {
    await wdb.raw(query);
  }
}

2. ๊ด€๊ณ„ ์ž๋™ ์ถ”์ 

BelongsTo ๊ด€๊ณ„๋ฅผ ์ž๋™์œผ๋กœ ์ถ”์ ํ•˜์—ฌ ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ชจ๋‘ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค.
# User #1์„ ๊ฐ€์ ธ์˜ค๋ฉด...
pnpm sonamu fixture import User 1

# ์ž๋™์œผ๋กœ ๊ฐ€์ ธ์˜ด:
# - User #1
# - User #1์˜ Profile (OneToOne)
# - Profile์˜ ๊ด€๋ จ ๋ฐ์ดํ„ฐ
๋‚ด๋ถ€ ๋™์ž‘:
async function getImportQueries(
  entityId: string,
  field: string,
  id: number
): Promise<string[]> {
  // ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€
  const recordKey = `${entityId}#${field}#${id}`;
  if (this.visitedRecords.has(recordKey)) {
    return [];
  }
  this.visitedRecords.add(recordKey);

  // ๋ ˆ์ฝ”๋“œ ๊ฐ€์ ธ์˜ค๊ธฐ
  const entity = EntityManager.get(entityId);
  const [row] = await wdb(entity.table).where(field, id).limit(1);

  if (!row) {
    throw new Error(`${entityId}#${id} row๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.`);
  }

  // BelongsTo ๊ด€๊ณ„ ์ฒ˜๋ฆฌ
  const relatedQueries = [];
  for (const [, relation] of Object.entries(entity.relations)) {
    if (isBelongsToOneRelationProp(relation)) {
      const relatedId = row[`${relation.name}_id`];
      if (relatedId) {
        // ์žฌ๊ท€์ ์œผ๋กœ ๊ด€๋ จ ๋ฐ์ดํ„ฐ ๊ฐ€์ ธ์˜ค๊ธฐ
        relatedQueries.push(
          ...(await this.getImportQueries(relation.with, "id", relatedId))
        );
      }
    }
  }

  // INSERT ์ฟผ๋ฆฌ ์ƒ์„ฑ
  const selfQuery = `INSERT IGNORE INTO fixture.${entity.table} 
    (SELECT * FROM production.${entity.table} WHERE id = ${id})`;

  return [...relatedQueries, selfQuery];
}

3. ์ž๋™ ๋™๊ธฐํ™”

import ํ›„ ์ž๋™์œผ๋กœ Fixture DB โ†’ Test DB ๋™๊ธฐํ™”๊ฐ€ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
async function fixture_import(entityId: string, recordIds: number[]) {
  await setupFixtureManager();

  // 1. ํ”„๋กœ๋•์…˜ โ†’ Fixture DB
  await FixtureManager.importFixture(entityId, recordIds);

  // 2. Fixture DB โ†’ Test DB
  await FixtureManager.sync();
}

์‹คํ–‰ ์˜ˆ์ œ

์‚ฌ์šฉ์ž ๊ฐ€์ ธ์˜ค๊ธฐ

pnpm sonamu fixture import User 1,2,3

{ entityId: 'User', field: 'id', id: 1 }
{ entityId: 'Profile', field: 'id', id: 1 }
INSERT IGNORE INTO fixture.profiles ...
INSERT IGNORE INTO fixture.users ...

{ entityId: 'User', field: 'id', id: 2 }
{ entityId: 'Profile', field: 'id', id: 2 }
INSERT IGNORE INTO fixture.profiles ...
INSERT IGNORE INTO fixture.users ...

โœ… Import complete! Syncing to test DB...

๊ฒŒ์‹œ๊ธ€ ๊ฐ€์ ธ์˜ค๊ธฐ

$ pnpm sonamu fixture import Post 42

{ entityId: 'Post', field: 'id', id: 42 }
{ entityId: 'User', field: 'id', id: 5 }
{ entityId: 'Profile', field: 'id', id: 5 }
{ entityId: 'Category', field: 'id', id: 10 }

INSERT IGNORE INTO fixture.profiles ...
INSERT IGNORE INTO fixture.users ...
INSERT IGNORE INTO fixture.categories ...
INSERT IGNORE INTO fixture.posts ...

โœ… Import complete!

๋ณต์žกํ•œ ๊ด€๊ณ„ ์ฒ˜๋ฆฌ

HasMany ๊ด€๊ณ„

# User #1๊ณผ ๊ทธ์˜ ๋ชจ๋“  Posts๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด
# 1. User ๊ฐ€์ ธ์˜ค๊ธฐ
pnpm sonamu fixture import User 1

# 2. User์˜ Posts๋ฅผ ๋ณ„๋„๋กœ ๊ฐ€์ ธ์˜ค๊ธฐ
pnpm sonamu fixture import Post 10,11,12

# HasMany๋Š” ์ž๋™์œผ๋กœ ์ถ”์ ๋˜์ง€ ์•Š์Œ
# (๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ๊ฐ€ ๊ฐ€์ ธ์™€์งˆ ์ˆ˜ ์žˆ์Œ)

ManyToMany ๊ด€๊ณ„

# Post #1๊ณผ ๊ด€๋ จ Tags๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด
# 1. Post ๊ฐ€์ ธ์˜ค๊ธฐ
pnpm sonamu fixture import Post 1

# 2. ์ค‘๊ฐ„ ํ…Œ์ด๋ธ” ๋ฐ์ดํ„ฐ๋Š” ์ˆ˜๋™์œผ๋กœ ์ฒ˜๋ฆฌ
# (๋˜๋Š” fixture.ts์—์„œ ์ง์ ‘ ์ƒ์„ฑ)

์ค‘๋ณต ์ฒ˜๋ฆฌ

INSERT IGNORE๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ค‘๋ณต์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
# User #1์„ ๋‘ ๋ฒˆ ๊ฐ€์ ธ์™€๋„ ์•ˆ์ „
pnpm sonamu fixture import User 1
pnpm sonamu fixture import User 1

# ๊ฒฐ๊ณผ: User #1์€ ํ•œ ๋ฒˆ๋งŒ ์ €์žฅ๋จ

์ˆœํ™˜ ์ฐธ์กฐ ๋ฐฉ์ง€

๋ฐฉ๋ฌธํ•œ ๋ ˆ์ฝ”๋“œ๋ฅผ ์ถ”์ ํ•˜์—ฌ ์ˆœํ™˜ ์ฐธ์กฐ๋ฅผ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค.
// ์ˆœํ™˜ ์ฐธ์กฐ ์˜ˆ์‹œ:
// User #1 โ†’ Profile #1
// Profile #1 โ†’ User #1 (์—ญ์ฐธ์กฐ)

// ๋ฐฉ๋ฌธ ๊ธฐ๋ก์œผ๋กœ ๋ฐฉ์ง€
private visitedRecords = new Set<string>();

const recordKey = `${entityId}#${field}#${id}`;
if (this.visitedRecords.has(recordKey)) {
  return []; // ์ด๋ฏธ ๋ฐฉ๋ฌธํ•จ - ์Šคํ‚ต
}
this.visitedRecords.add(recordKey);

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

1. ์ตœ์†Œ ๋ฐ์ดํ„ฐ

# โœ… ์˜ฌ๋ฐ”๋ฅธ ๋ฐฉ๋ฒ•: ํ•„์š”ํ•œ ๊ฒƒ๋งŒ
pnpm sonamu fixture import User 1,2

# โŒ ์ž˜๋ชป๋œ ๋ฐฉ๋ฒ•: ๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ
pnpm sonamu fixture import User 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15

2. ๋Œ€ํ‘œ ์ผ€์ด์Šค

# ๋‹ค์–‘ํ•œ ์ผ€์ด์Šค๋ฅผ ํฌํ•จํ•˜๋Š” ๋ฐ์ดํ„ฐ ์„ ํƒ
pnpm sonamu fixture import User 1    # ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž
pnpm sonamu fixture import User 100  # ๊ด€๋ฆฌ์ž
pnpm sonamu fixture import User 500  # ํ”„๋ฆฌ๋ฏธ์—„ ์‚ฌ์šฉ์ž

3. fixture.ts์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ

// api/src/testing/fixture.ts
import { createFixtureLoader } from "sonamu/test";

export const loadFixtures = createFixtureLoader({
  // ํ”„๋กœ๋•์…˜์—์„œ ๊ฐ€์ ธ์˜จ ์‹ค์ œ ๋ฐ์ดํ„ฐ ์‚ฌ์šฉ
  realUser01: async () => {
    const userModel = new UserModel();
    // ID 1์€ ์ด๋ฏธ fixture DB์— ์žˆ๋‹ค๊ณ  ๊ฐ€์ •
    const { user } = await userModel.getUser("C", 1);
    return user;
  },

  // ํ…Œ์ŠคํŠธ์šฉ ์ƒˆ ๋ฐ์ดํ„ฐ ์ƒ์„ฑ
  testUser: async () => {
    const userModel = new UserModel();
    const { user } = await userModel.create({
      username: "test",
      email: "[email protected]",
      password: "password",
    });
    return user;
  },
});

๋ฌธ์ œ ํ•ด๊ฒฐ

๋ ˆ์ฝ”๋“œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ

Error: User#999 row๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.
ํ•ด๊ฒฐ:
  • ํ”„๋กœ๋•์…˜ DB์— ํ•ด๋‹น ID๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ
  • ์˜ฌ๋ฐ”๋ฅธ Entity ์ด๋ฆ„ ์‚ฌ์šฉ

๊ถŒํ•œ ์˜ค๋ฅ˜

Error: Access denied for user 'readonly'
ํ•ด๊ฒฐ:
-- Fixture DB ์‚ฌ์šฉ์ž์—๊ฒŒ ์“ฐ๊ธฐ ๊ถŒํ•œ ๋ถ€์—ฌ
GRANT ALL PRIVILEGES ON fixture.* TO 'user'@'%';

๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ

# ํ•œ ๋ฒˆ์— ๋„ˆ๋ฌด ๋งŽ์€ ID๋ฅผ ๊ฐ€์ ธ์˜ค๋ฉด ๋А๋ฆผ
pnpm sonamu fixture import User 1,2,3,...,100

# ํ•ด๊ฒฐ: ์—ฌ๋Ÿฌ ๋ฒˆ ๋‚˜๋ˆ ์„œ ์‹คํ–‰
pnpm sonamu fixture import User 1,2,3,4,5
pnpm sonamu fixture import User 6,7,8,9,10

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

Fixture ๋กœ๋”ฉ ์‹œ ์ฃผ์˜์‚ฌํ•ญ:
  1. ์ตœ์†Œ ๋ฐ์ดํ„ฐ: ํ•„์š”ํ•œ ์ตœ์†Œํ•œ์˜ ๋ ˆ์ฝ”๋“œ๋งŒ ๊ฐ€์ ธ์˜ค๊ธฐ
  2. ๊ด€๊ณ„ ํ™•์ธ: BelongsTo๋Š” ์ž๋™, HasMany๋Š” ์ˆ˜๋™
  3. ์ค‘๋ณต ์•ˆ์ „: INSERT IGNORE๋กœ ์ค‘๋ณต ๋ฐฉ์ง€
  4. ์ˆœํ™˜ ์ฐธ์กฐ: ์ž๋™์œผ๋กœ ์ฒ˜๋ฆฌ๋จ
  5. ๊ถŒํ•œ ํ™•์ธ: Fixture DB์— ์“ฐ๊ธฐ ๊ถŒํ•œ ํ•„์š”

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