integration-test-builder

安装量: 37
排名: #18929

安装

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

Integration Test Builder

Build comprehensive integration tests for APIs and database flows.

Test Harness Setup // 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: "test@example.com", name: "Test User", }, }); } }

API Integration Tests // 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: "new@example.com", 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: "test@example.com", // 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();
});

}); });

Database Transaction Tests // 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);

}); });

Authentication Tests // 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: "auth@example.com", password: await hash("password123"), }, });

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

  // Assert
  expect(response.body.token).toBeDefined();
  expect(response.body.user.email).toBe("auth@example.com");
});

it("should reject invalid password", async () => {
  // Act
  const response = await request(app)
    .post("/api/auth/login")
    .send({
      email: "test@example.com",
      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: "test@example.com",
    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);
});

}); });

Fixtures Management // tests/fixtures/users.ts export const userFixtures = { admin: { email: "admin@example.com", name: "Admin User", role: "ADMIN", }, regularUser: { email: "user@example.com", name: "Regular User", role: "USER", }, testUser: { email: "test@example.com", 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-Friendly Strategy

.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

Parallel Test Execution // 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}; }

Best Practices Isolated tests: Each test can run independently Clean state: Clear database between tests Fast fixtures: Minimal data seeding Transactions: Test rollbacks explicitly Real database: Don't mock database in integration tests CI-ready: Use Docker containers Parallel execution: Independent test databases Output Checklist Test harness created Database setup/teardown Fixture management API endpoint tests Database transaction tests Authentication tests Error case coverage CI workflow configured Parallel execution support Clear test naming

返回排行榜