mcpField
I
integration-test-builder

integration-test-builder

github.com/patricio0312rev测试与质量63254 次安装41 周前更新
代码仓库

关于 integration-test-builder

integration-test-builder 是用于构建 API 和数据库集成测试的技能,涵盖测试框架、数据夹具、认证与事务测试,以及 CI 和并行执行配置。

主要功能

  1. 01设置测试框架
  2. 02API 端点集成测试
  3. 03数据库事务测试
  4. 04认证与错误场景覆盖
  5. 05数据夹具与数据库清理
  6. 06CI 与并行执行

适用场景

  1. 01创建 API 集成测试套件
  2. 02测试数据库订单流程与回滚
  3. 03为 CI 准备集成测试
  4. 04并行运行相互隔离的测试

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/patricio0312rev/skills --skill integration-test-builder

安装后在 Claude Code 中输入/integration-test-builder确认已加载。

详细介绍

集成测试构建器

为 API 和数据库流程构建全面的集成测试。

测试框架设置

// tests/setup/test-harness.ts
import { PrismaClient } from "@prisma/client";
import { execSync } from "child_process";

export class TestHarness {
  prisma: PrismaClient;

  async setup() {
    // Setup test database
    process.env.DATABASE_URL = process.env.TEST_DATABASE_URL;

    // Run migrations
    execSync("npx prisma migrate deploy");

    // Initialize Prisma client
    this.prisma = new PrismaClient();

    // Clear all data
    await this.clearDatabase();
  }

  async teardown() {
    await this.prisma.$disconnect();
  }

  async clearDatabase() {
    const tables = await this.prisma.$queryRaw<{ tablename: string }[]>`
      SELECT tablename FROM pg_tables WHERE schemaname = 'public'
    `;

    for (const { tablename } of tables) {
      if (tablename !== "_prisma_migrations") {
        await this.prisma.$executeRawUnsafe(
          `TRUNCATE TABLE "${tablename}" CASCADE`
        );
      }
    }
  }

  async seedFixtures() {
    // Seed test data
    await this.prisma.user.create({
      data: {
        email: "[email protected]",
        name: "Test User",
      },
    });
  }
}

API 集成测试

// tests/api/users.test.ts
import request from "supertest";
import { app } from "@/app";
import { TestHarness } from "../setup/test-harness";

describe("User API", () => {
  let harness: TestHarness;

  beforeAll(async () => {
    harness = new TestHarness();
    await harness.setup();
  });

  afterAll(async () => {
    await harness.teardown();
  });

  beforeEach(async () => {
    await harness.clearDatabase();
    await harness.seedFixtures();
  });

  describe("POST /api/users", () => {
    it("should create new user", async () => {
      // Arrange
      const userData = {
        email: "[email protected]",
        name: "New User",
      };

      // Act
      const response = await request(app)
        .post("/api/users")
        .send(userData)
        .expect(201);

      // Assert
      expect(response.body).toMatchObject({
        email: userData.email,
        name: userData.name,
      });
      expect(response.body.id).toBeDefined();

      // Verify in database
      const user = await harness.prisma.user.findUnique({
        where: { email: userData.email },
      });
      expect(user).toBeDefined();
      expect(user!.name).toBe(userData.name);
    });

    it("should return 400 for invalid email", async () => {
      // Arrange
      const userData = {
        email: "invalid-email",
        name: "Test User",
      };

      // Act
      const response = await request(app)
        .post("/api/users")
        .send(userData)
        .expect(400);

      // Assert
      expect(response.body.error).toContain("Invalid email");
    });

    it("should return 409 for duplicate email", async () => {
      // Arrange
      const userData = {
        email: "[email protected]", // Already exists
        name: "Duplicate User",
      };

      // Act
      const response = await request(app)
        .post("/api/users")
        .send(userData)
        .expect(409);

      // Assert
      expect(response.body.error).toContain("already exists");
    });
  });

  describe("GET /api/users/:id", () => {
    it("should get user by id", async () => {
      // Arrange
      const user = await harness.prisma.user.findFirst();

      // Act
      const response = await request(app)
        .get(`/api/users/${user!.id}`)
        .expect(200);

      // Assert
      expect(response.body).toMatchObject({
        id: user!.id,
        email: user!.email,
        name: user!.name,
      });
    });

    it("should return 404 for non-existent user", async () => {
      // Act
      const response = await request(app).get("/api/users/99999").expect(404);

      // Assert
      expect(response.body.error).toContain("not found");
    });
  });

  describe("PUT /api/users/:id", () => {
    it("should update user", async () => {
      // Arrange
      const user = await harness.prisma.user.findFirst();
      const updates = { name: "Updated Name" };

      // Act
      const response = await request(app)
        .put(`/api/users/${user!.id}`)
        .send(updates)
        .expect(200);

      // Assert
      expect(response.body.name).toBe("Updated Name");

      // Verify in database
      const updatedUser = await harness.prisma.user.findUnique({
        where: { id: user!.id },
      });
      expect(updatedUser!.name).toBe("Updated Name");
    });
  });

  describe("DELETE /api/users/:id", () => {
    it("should delete user", async () => {
      // Arrange
      const user = await harness.prisma.user.findFirst();

      // Act
      await request(app).delete(`/api/users/${user!.id}`).expect(204);

      // Assert - verify deletion in database
      const deletedUser = await harness.prisma.user.findUnique({
        where: { id: user!.id },
      });
      expect(deletedUser).toBeNull();
    });
  });
});

数据库事务测试

// tests/integration/order-flow.test.ts
describe("Order Flow", () => {
  it("should create order with items in transaction", async () => {
    // Arrange
    const user = await harness.prisma.user.findFirst();
    const product = await harness.prisma.product.create({
      data: {
        name: "Test Product",
        price: 99.99,
        stock: 10,
      },
    });

    const orderData = {
      userId: user!.id,
      items: [
        {
          productId: product.id,
          quantity: 2,
          price: product.price,
        },
      ],
    };

    // Act
    const response = await request(app)
      .post("/api/orders")
      .send(orderData)
      .expect(201);

    // Assert
    const order = await harness.prisma.order.findUnique({
      where: { id: response.body.id },
      include: { items: true },
    });

    expect(order).toBeDefined();
    expect(order!.items).toHaveLength(1);
    expect(order!.items[0].quantity).toBe(2);

    // Verify stock was decremented
    const updatedProduct = await harness.prisma.product.findUnique({
      where: { id: product.id },
    });
    expect(updatedProduct!.stock).toBe(8); // 10 - 2
  });

  it("should rollback transaction if order creation fails", async () => {
    // Arrange
    const user = await harness.prisma.user.findFirst();
    const product = await harness.prisma.product.create({
      data: {
        name: "Test Product",
        price: 99.99,
        stock: 1, // Only 1 in stock
      },
    });

    const orderData = {
      userId: user!.id,
      items: [
        {
          productId: product.id,
          quantity: 10, // Requesting more than available
          price: product.price,
        },
      ],
    };

    // Act
    await request(app).post("/api/orders").send(orderData).expect(400);

    // Assert - verify rollback
    const orders = await harness.prisma.order.findMany();
    expect(orders).toHaveLength(0);

    // Verify stock unchanged
    const unchangedProduct = await harness.prisma.product.findUnique({
      where: { id: product.id },
    });
    expect(unchangedProduct!.stock).toBe(1);
  });
});

身份验证测试

// tests/integration/auth.test.ts
describe("Authentication", () => {
  describe("POST /api/auth/login", () => {
    it("should login with valid credentials", async () => {
      // Arrange
      await harness.prisma.user.create({
        data: {
          email: "[email protected]",
          password: await hash("password123"),
        },
      });

      // Act
      const response = await request(app)
        .post("/api/auth/login")
        .send({
          email: "[email protected]",
          password: "password123",
        })
        .expect(200);

      // Assert
      expect(response.body.token).toBeDefined();
      expect(response.body.user.email).toBe("[email protected]");
    });

    it("should reject invalid password", async () => {
      // Act
      const response = await request(app)
        .post("/api/auth/login")
        .send({
          email: "[email protected]",
          password: "wrong-password",
        })
        .expect(401);

      // Assert
      expect(response.body.error).toContain("Invalid credentials");
    });
  });

  describe("Protected routes", () => {
    let authToken: string;

    beforeEach(async () => {
      // Login to get token
      const response = await request(app).post("/api/auth/login").send({
        email: "[email protected]",
        password: "password123",
      });

      authToken = response.body.token;
    });

    it("should access protected route with valid token", async () => {
      await request(app)
        .get("/api/profile")
        .set("Authorization", `Bearer ${authToken}`)
        .expect(200);
    });

    it("should reject request without token", async () => {
      await request(app).get("/api/profile").expect(401);
    });

    it("should reject request with invalid token", async () => {
      await request(app)
        .get("/api/profile")
        .set("Authorization", "Bearer invalid-token")
        .expect(401);
    });
  });
});

测试夹具管理

// tests/fixtures/users.ts
export const userFixtures = {
  admin: {
    email: "[email protected]",
    name: "Admin User",
    role: "ADMIN",
  },
  regularUser: {
    email: "[email protected]",
    name: "Regular User",
    role: "USER",
  },
  testUser: {
    email: "[email protected]",
    name: "Test User",
    role: "USER",
  },
};

// tests/fixtures/products.ts
export const productFixtures = {
  laptop: {
    name: "MacBook Pro",
    price: 2499.99,
    stock: 10,
    category: "Electronics",
  },
  phone: {
    name: "iPhone 15",
    price: 999.99,
    stock: 50,
    category: "Electronics",
  },
};

// Usage in tests
await harness.prisma.user.create({
  data: userFixtures.admin,
});

适用于 CI 的策略

# .github/workflows/integration-tests.yml
name: Integration Tests

on: [push, pull_request]

services:
  postgres:
    image: postgres:15
    env:
      POSTGRES_USER: test
      POSTGRES_PASSWORD: test
      POSTGRES_DB: test_db
    ports:
      - 5432:5432
    options: >-
      --health-cmd pg_isready
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"

      - run: npm ci

      - name: Run migrations
        run: npx prisma migrate deploy
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/test_db

      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgresql://test:test@localhost:5432/test_db

并行测试执行

// vitest.config.ts
export default defineConfig({
  test: {
    pool: "forks",
    poolOptions: {
      forks: {
        singleFork: false, // Run tests in parallel
      },
    },
    isolate: true, // Isolate each test file
    setupFiles: ["./tests/setup/global-setup.ts"],
  },
});

// Ensure each test file uses separate database
const TEST_DB_PREFIX = "test_db_";

function getDatabaseUrl(): string {
  const workerId = process.env.VITEST_WORKER_ID || "1";
  return `postgresql://test:test@localhost:5432/${TEST_DB_PREFIX}${workerId}`;
}

最佳实践

  1. 测试隔离:每项测试都可以独立运行
  2. 状态清理:在测试之间清空数据库
  3. 快速夹具:尽量减少数据填充
  4. 事务:明确测试回滚
  5. 真实数据库:集成测试中不要模拟数据库
  6. 适用于 CI:使用 Docker 容器
  7. 并行执行:使用相互独立的测试数据库

输出检查清单

  • 已创建测试框架
  • 已设置数据库并配置清理流程
  • 已管理测试夹具
  • 已测试 API 端点
  • 已测试数据库事务
  • 已测试身份验证
  • 已覆盖错误场景
  • 已配置 CI 工作流
  • 支持并行执行
  • 测试命名清晰

评论

支持 Markdown