⚠️ Alpha内测版本警告:此为早期内部构建版本,尚不完整且可能存在错误,欢迎大家提Issue反馈问题或建议
Skip to content

9.3.3 测试前准备好数据——Seed 数据:测试用例的数据准备

好的测试数据准备策略能让测试代码更简洁、更易维护。

测试数据准备模式

工厂模式(推荐)

创建可复用的数据工厂,灵活生成测试数据。

typescript
// test/factories/user.factory.ts
import { prisma } from '@/lib/prisma';
import { faker } from '@faker-js/faker';
import type { User, Prisma } from '@prisma/client';

type UserOverrides = Partial<Prisma.UserCreateInput>;

export async function createUser(overrides: UserOverrides = {}): Promise<User> {
  return prisma.user.create({
    data: {
      email: faker.internet.email(),
      name: faker.person.fullName(),
      password: faker.internet.password(),
      ...overrides,
    },
  });
}

export async function createUsers(
  count: number,
  overrides: UserOverrides = {}
): Promise<User[]> {
  return Promise.all(
    Array.from({ length: count }, () => createUser(overrides))
  );
}
typescript
// test/factories/order.factory.ts
import { prisma } from '@/lib/prisma';
import { faker } from '@faker-js/faker';
import { createUser } from './user.factory';
import { createProduct } from './product.factory';

interface OrderOptions {
  userId?: string;
  itemCount?: number;
  status?: string;
}

export async function createOrder(options: OrderOptions = {}) {
  const user = options.userId
    ? await prisma.user.findUnique({ where: { id: options.userId } })
    : await createUser();

  const products = await Promise.all(
    Array.from({ length: options.itemCount || 2 }, () => createProduct())
  );

  return prisma.order.create({
    data: {
      userId: user!.id,
      status: options.status || 'PENDING',
      items: {
        create: products.map((p) => ({
          productId: p.id,
          quantity: faker.number.int({ min: 1, max: 5 }),
          price: p.price,
        })),
      },
    },
    include: {
      items: true,
      user: true,
    },
  });
}

使用工厂

typescript
// __tests__/services/order.service.test.ts
import { createUser } from '@/test/factories/user.factory';
import { createOrder } from '@/test/factories/order.factory';
import { createProduct } from '@/test/factories/product.factory';

describe('OrderService', () => {
  it('应计算订单总额', async () => {
    const order = await createOrder({ itemCount: 3 });
    
    const total = await orderService.calculateTotal(order.id);
    
    const expectedTotal = order.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    );
    expect(total).toBe(expectedTotal);
  });

  it('VIP 用户应享受折扣', async () => {
    const vipUser = await createUser({ role: 'VIP' });
    const order = await createOrder({ userId: vipUser.id });
    
    const total = await orderService.calculateTotal(order.id);
    
    // VIP 用户享受 10% 折扣
    expect(total).toBeLessThan(order.items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    ));
  });
});

固定数据 Fixtures

适合需要精确控制数据内容的场景。

typescript
// test/fixtures/users.ts
export const testUsers = {
  admin: {
    id: 'user-admin-001',
    email: 'admin@test.com',
    name: 'Admin User',
    role: 'ADMIN',
  },
  customer: {
    id: 'user-customer-001',
    email: 'customer@test.com',
    name: 'Test Customer',
    role: 'CUSTOMER',
  },
  vip: {
    id: 'user-vip-001',
    email: 'vip@test.com',
    name: 'VIP User',
    role: 'VIP',
  },
};

// test/fixtures/products.ts
export const testProducts = {
  laptop: {
    id: 'prod-laptop-001',
    name: 'MacBook Pro',
    price: 9999,
    stock: 100,
  },
  phone: {
    id: 'prod-phone-001',
    name: 'iPhone 15',
    price: 6999,
    stock: 50,
  },
};
typescript
// test/helpers/seed-fixtures.ts
import { prisma } from '@/lib/prisma';
import { testUsers } from '../fixtures/users';
import { testProducts } from '../fixtures/products';

export async function seedFixtures() {
  // 创建用户
  for (const user of Object.values(testUsers)) {
    await prisma.user.upsert({
      where: { id: user.id },
      update: user,
      create: user,
    });
  }

  // 创建产品
  for (const product of Object.values(testProducts)) {
    await prisma.product.upsert({
      where: { id: product.id },
      update: product,
      create: product,
    });
  }
}

使用 Fixtures

typescript
// __tests__/services/order.service.test.ts
import { testUsers } from '@/test/fixtures/users';
import { testProducts } from '@/test/fixtures/products';
import { seedFixtures } from '@/test/helpers/seed-fixtures';

describe('OrderService with Fixtures', () => {
  beforeAll(async () => {
    await seedFixtures();
  });

  it('管理员可以查看所有订单', async () => {
    const orders = await orderService.getAll(testUsers.admin.id);
    expect(Array.isArray(orders)).toBe(true);
  });

  it('应正确计算商品总价', async () => {
    const order = await orderService.create(testUsers.customer.id, [
      { productId: testProducts.laptop.id, quantity: 1 },
      { productId: testProducts.phone.id, quantity: 2 },
    ]);

    expect(order.total).toBe(9999 + 6999 * 2);
  });
});

Builder 模式

适合复杂对象的构建。

typescript
// test/builders/order.builder.ts
import { prisma } from '@/lib/prisma';
import { createUser } from '../factories/user.factory';
import { createProduct } from '../factories/product.factory';

export class OrderBuilder {
  private userId?: string;
  private items: { productId: string; quantity: number }[] = [];
  private status = 'PENDING';
  private discountCode?: string;

  withUser(userId: string): this {
    this.userId = userId;
    return this;
  }

  withItem(productId: string, quantity: number): this {
    this.items.push({ productId, quantity });
    return this;
  }

  withStatus(status: string): this {
    this.status = status;
    return this;
  }

  withDiscount(code: string): this {
    this.discountCode = code;
    return this;
  }

  async build() {
    const userId = this.userId || (await createUser()).id;
    
    if (this.items.length === 0) {
      const product = await createProduct();
      this.items.push({ productId: product.id, quantity: 1 });
    }

    return prisma.order.create({
      data: {
        userId,
        status: this.status,
        discountCode: this.discountCode,
        items: {
          create: this.items.map((item) => ({
            productId: item.productId,
            quantity: item.quantity,
          })),
        },
      },
      include: { items: true },
    });
  }
}

使用 Builder

typescript
// __tests__/services/order.service.test.ts
import { OrderBuilder } from '@/test/builders/order.builder';

describe('OrderService with Builder', () => {
  it('应用折扣码', async () => {
    const order = await new OrderBuilder()
      .withDiscount('SAVE20')
      .withItem('prod-1', 2)
      .withStatus('PENDING')
      .build();

    const total = await orderService.calculateTotal(order.id);
    
    // 验证折扣已应用
    expect(order.discountCode).toBe('SAVE20');
  });
});

数据清理策略

typescript
// test/helpers/cleanup.ts
import { prisma } from '@/lib/prisma';

export async function cleanupTestData() {
  await prisma.orderItem.deleteMany();
  await prisma.order.deleteMany();
  await prisma.product.deleteMany();
  await prisma.user.deleteMany();
}

// jest.setup.ts
import { cleanupTestData } from './test/helpers/cleanup';

beforeEach(async () => {
  await cleanupTestData();
});

本节小结

测试数据准备的核心是可复用性和灵活性。工厂模式适合大多数场景,Fixtures 适合固定场景,Builder 模式适合复杂对象。选择合适的模式,让测试代码更清晰、更易维护。