DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Claude CodeでDBシーディング・テストフィクスチャを設計する:Factory・Faker・Prisma統合

テストコードで毎回const user = { id: 'test-user', name: 'テスト太郎', email: 'test@example.com', ... }と書いていませんか?Claude Codeを使ってFactory・Faker・Prismaを統合したテストフィクスチャを設計すると、テストデータの準備が劇的にシンプルになります。本記事では、現場で使えるパターンを体系的に紹介します。


CLAUDE.mdルール

プロジェクトのCLAUDE.mdに以下を記載することで、Claude Codeがテスト設計時に自動的にFactoryパターンを使ったコードを生成します。

## テスト設計ルール
- テストデータはFactoryパターンで生成する(オブジェクトリテラルの直書き禁止)
- Factory.build()はDB接続なしのユニットテスト用、Factory.create()はDB接続ありの結合テスト用
- Faker.jsは日本語ロケール(ja)を使用し、本番に近いリアルなデータを生成する
- 関連エンティティは自動生成(OrderFactoryはUserとProductを自動create)
- createTestWorld()ヘルパーで複合シナリオのデータセットアップを共通化する
Enter fullscreen mode Exit fullscreen mode

Factory.build() vs Factory.create() の使い分け

Factoryの基本パターンから始めます。build()はメモリ内オブジェクト、create()はDB保存まで行います。

// tests/factories/user.factory.ts
import { faker } from '@faker-js/faker/locale/ja';
import { prisma } from '../../src/lib/prisma';
import type { User } from '@prisma/client';

type UserOverride = Partial<Omit<User, 'id' | 'createdAt' | 'updatedAt'>>;

export const UserFactory = {
  // DB接続なし — ユニットテストで高速に使える
  build: (overrides: UserOverride = {}): Omit<User, 'id' | 'createdAt' | 'updatedAt'> => ({
    name:      faker.person.fullName(),
    email:     faker.internet.email(),
    role:      'USER',
    isActive:  true,
    ...overrides,
  }),

  // DB保存あり — 結合テスト・E2Eテストで使う
  create: async (overrides: UserOverride = {}): Promise<User> => {
    return prisma.user.create({
      data: UserFactory.build(overrides),
    });
  },

  // 複数件一括生成
  createMany: async (count: number, overrides: UserOverride = {}): Promise<User[]> => {
    return Promise.all(
      Array.from({ length: count }, () => UserFactory.create(overrides))
    );
  },
};
Enter fullscreen mode Exit fullscreen mode

ユニットテストではbuild()を使うことでDB不要・高速実行が可能になります。

// tests/unit/user.service.test.ts
import { UserFactory } from '../factories/user.factory';

describe('UserService.validateAge', () => {
  it('未成年ユーザーを弾く', () => {
    const user = UserFactory.build({ birthYear: 2015 });  // DB接続なし
    expect(validateAge(user)).toBe(false);
  });
});
Enter fullscreen mode Exit fullscreen mode

Faker.jsの日本語ロケール活用

Faker.jsのjaロケールを使うと、本番データに近いリアルなテストデータが生成できます。

// tests/factories/product.factory.ts
import { faker } from '@faker-js/faker/locale/ja';
import { prisma } from '../../src/lib/prisma';
import type { Product } from '@prisma/client';

type ProductOverride = Partial<Omit<Product, 'id' | 'createdAt' | 'updatedAt'>>;

export const ProductFactory = {
  build: (overrides: ProductOverride = {}) => ({
    name:        faker.commerce.productName(),
    description: faker.commerce.productDescription(),
    price:       Number(faker.commerce.price({ min: 100, max: 50000 })),
    category:    faker.helpers.arrayElement(['電子機器', '書籍', '食品', '衣料品']),
    stock:       faker.number.int({ min: 0, max: 999 }),
    sku:         faker.string.alphanumeric(8).toUpperCase(),
    isPublished: true,
    ...overrides,
  }),

  create: async (overrides: ProductOverride = {}): Promise<Product> => {
    return prisma.product.create({
      data: ProductFactory.build(overrides),
    });
  },
};

// 住所・電話番号なども日本語で生成できる
// faker.location.city()   → 例: 「大阪市」
// faker.phone.number()    → 例: 「090-1234-5678」
// faker.company.name()    → 例: 「株式会社山田商事」
Enter fullscreen mode Exit fullscreen mode

OrderFactoryで関連エンティティを自動生成

注文(Order)を作るにはUserとProductが必要ですが、OrderFactoryが自動的に関連エンティティを生成します。

// tests/factories/order.factory.ts
import { faker } from '@faker-js/faker/locale/ja';
import { prisma } from '../../src/lib/prisma';
import { UserFactory } from './user.factory';
import { ProductFactory } from './product.factory';
import type { Order, User, Product } from '@prisma/client';

type OrderWithRelations = Order & { user: User; product: Product };
type OrderOverride = {
  userId?:    string;
  productId?: string;
  quantity?:  number;
  status?:    Order['status'];
};

export const OrderFactory = {
  create: async (overrides: OrderOverride = {}): Promise<OrderWithRelations> => {
    // UserとProductが未指定なら自動生成
    const user = overrides.userId
      ? await prisma.user.findUniqueOrThrow({ where: { id: overrides.userId } })
      : await UserFactory.create();

    const product = overrides.productId
      ? await prisma.product.findUniqueOrThrow({ where: { id: overrides.productId } })
      : await ProductFactory.create();

    const quantity = overrides.quantity ?? faker.number.int({ min: 1, max: 10 });

    const order = await prisma.order.create({
      data: {
        userId:     user.id,
        productId:  product.id,
        quantity,
        totalPrice: product.price * quantity,
        status:     overrides.status ?? 'PENDING',
      },
      include: { user: true, product: true },
    });

    return order;
  },
};

// テストではOrderFactory.create()一発でUser+Product+Order全て揃う
// const order = await OrderFactory.create({ status: 'COMPLETED' });
// order.user.name    → 自動生成されたユーザー名
// order.product.name → 自動生成された商品名
Enter fullscreen mode Exit fullscreen mode

createTestWorld() — 複合シナリオのデータセットアップ

複数のテストで同じデータセットが必要な場合は、createTestWorld()ヘルパーで共通化します。

// tests/helpers/test-world.ts
import { UserFactory }    from '../factories/user.factory';
import { ProductFactory } from '../factories/product.factory';
import { OrderFactory }   from '../factories/order.factory';
import type { User, Product, Order } from '@prisma/client';

interface TestWorld {
  adminUser:       User;
  regularUser:     User;
  premiumProduct:  Product;
  standardProduct: Product;
  pendingOrder:    Order & { user: User; product: Product };
  completedOrder:  Order & { user: User; product: Product };
}

export async function createTestWorld(): Promise<TestWorld> {
  const [adminUser, regularUser] = await Promise.all([
    UserFactory.create({ role: 'ADMIN' }),
    UserFactory.create({ role: 'USER' }),
  ]);

  const [premiumProduct, standardProduct] = await Promise.all([
    ProductFactory.create({ price: 9800, category: '電子機器' }),
    ProductFactory.create({ price: 980,  category: '書籍' }),
  ]);

  const [pendingOrder, completedOrder] = await Promise.all([
    OrderFactory.create({ userId: regularUser.id, productId: standardProduct.id, status: 'PENDING' }),
    OrderFactory.create({ userId: regularUser.id, productId: premiumProduct.id,  status: 'COMPLETED' }),
  ]);

  return { adminUser, regularUser, premiumProduct, standardProduct, pendingOrder, completedOrder };
}

// テストでの使用例
// describe('OrderService', () => {
//   let world: TestWorld;
//   beforeEach(async () => { world = await createTestWorld(); });
//
//   it('管理者は全注文を取得できる', async () => {
//     const orders = await orderService.findAll(world.adminUser.id);
//     expect(orders.length).toBeGreaterThanOrEqual(2);
//   });
// });
Enter fullscreen mode Exit fullscreen mode

まとめ

  • Factory.build() / create() を使い分けることで、ユニットテストはDB不要・高速、結合テストはリアルなデータで検証できる
  • Faker.jsのjaロケールにより、日本語の氏名・住所・商品名などが自動生成でき、本番に近いテストシナリオが構築できる
  • OrderFactoryの関連エンティティ自動生成で、テスト記述のボイラープレートが大幅に削減され、テストの意図が明確になる
  • createTestWorld()ヘルパーで複合シナリオを共通化すると、beforeEachの肥大化を防ぎ、テストの可読性と保守性が向上する

さらに深く学ぶ

この記事で紹介したFactory・Prismaテストパターンを含む、Claude Codeのコードレビュー・設計支援プロンプトをまとめたCode Review Pack(¥980)をnoteで販売中です。

テスト設計・型安全・セキュリティレビューなど、現場で即使えるプロンプト集です。ぜひご活用ください。

Code Review Pack ¥980 — note

Top comments (0)