零度AI
高级20 分钟阅读

Claude Code 集成测试

使用 Claude Code 编写集成测试,验证多个模块间的协作和 API 端点

Claude Code集成测试SupertestAPI测试数据库测试

集成测试概述

集成测试验证多个模块协作的正确性。Claude Code 可以帮你实现 API 和数据库集成测试。

集成测试范围

模块间交互、API 端点、数据库操作、缓存交互、外部服务模拟。

API 集成测试

Express + Supertest

bash
帮我创建一个 API 集成测试配置,使用 Supertest 测试 Express 应用。
javascript
// src/test/integration/api/setup.js
import { beforeAll, afterAll, afterEach } from "vitest";
import supertest from "supertest";
import { app } from "../../app.js";
import { connectDB, disconnectDB } from "../../lib/db/mongodb.js";
import { seedTestData, clearTestData } from "../helpers/seed.js";

export const request = supertest(app);

beforeAll(async () => {
  await connectDB();
  await seedTestData();
});

afterAll(async () => {
  await disconnectDB();
});

afterEach(async () => {
  await clearTestData();
});

API 路由测试

javascript
// src/test/integration/api/user.test.js
import { describe, it, expect, beforeEach } from "vitest";
import { request } from "./setup.js";
import { createTestUser, generateToken } from "../helpers/testUtils.js";

describe("User API", () => {
  describe("POST /api/users/register", () => {
    it("应该成功注册新用户", async () => {
      const userData = {
        username: "newuser",
        email: "new@example.com",
        password: "Password123!",
      };

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

      expect(response.body.success).toBe(true);
      expect(response.body.data.user.username).toBe(userData.username);
      expect(response.body.data.token).toBeDefined();
    });

    it("邮箱已被注册时应该返回 400", async () => {
      const existingUser = await createTestUser();

      const response = await request
        .post("/api/users/register")
        .send({
          username: "another",
          email: existingUser.email,
          password: "Password123!",
        })
        .expect(400);

      expect(response.body.error).toContain("已被注册");
    });

    it("密码太短时应该返回 400", async () => {
      const response = await request
        .post("/api/users/register")
        .send({
          username: "test",
          email: "test@example.com",
          password: "123",
        })
        .expect(400);

      expect(response.body.error).toContain("密码");
    });

    it("无效邮箱格式应该返回 400", async () => {
      const response = await request
        .post("/api/users/register")
        .send({
          username: "test",
          email: "invalid-email",
          password: "Password123!",
        })
        .expect(400);

      expect(response.body.error).toContain("邮箱");
    });
  });

  describe("POST /api/users/login", () => {
    it("正确的凭据应该登录成功", async () => {
      const user = await createTestUser({
        password: "Password123!",
      });

      const response = await request
        .post("/api/users/login")
        .send({
          email: user.email,
          password: "Password123!",
        })
        .expect(200);

      expect(response.body.success).toBe(true);
      expect(response.body.data.token).toBeDefined();
    });

    it("错误的密码应该返回 401", async () => {
      const user = await createTestUser();

      await request
        .post("/api/users/login")
        .send({
          email: user.email,
          password: "WrongPassword!",
        })
        .expect(401);
    });

    it("不存在的用户应该返回 401", async () => {
      await request
        .post("/api/users/login")
        .send({
          email: "nonexistent@example.com",
          password: "Password123!",
        })
        .expect(401);
    });
  });

  describe("GET /api/users/me", () => {
    it("登录用户应该能获取自己的信息", async () => {
      const { user, token } = await createTestUser({
        username: "testuser",
        email: "me@example.com",
      });

      const response = await request
        .get("/api/users/me")
        .set("Authorization", `Bearer ${token}`)
        .expect(200);

      expect(response.body.data.username).toBe("testuser");
      expect(response.body.data.email).toBe("me@example.com");
    });

    it("未登录应该返回 401", async () => {
      await request.get("/api/users/me").expect(401);
    });

    it("无效 Token 应该返回 401", async () => {
      await request
        .get("/api/users/me")
        .set("Authorization", "Bearer invalid-token")
        .expect(401);
    });
  });

  describe("PUT /api/users/profile", () => {
    it("应该能更新个人资料", async () => {
      const { token } = await createTestUser();

      const response = await request
        .put("/api/users/profile")
        .set("Authorization", `Bearer ${token}`)
        .send({
          bio: "这是我的新简介",
          avatar: "https://example.com/new-avatar.jpg",
        })
        .expect(200);

      expect(response.body.data.bio).toBe("这是我的新简介");
    });

    it("用户名已存在时应该返回 400", async () => {
      const { token } = await createTestUser();
      const existingUser = await createTestUser({ username: "taken" });

      await request
        .put("/api/users/profile")
        .set("Authorization", `Bearer ${token}`)
        .send({ username: "taken" })
        .expect(400);
    });
  });
});

博客 API 测试

javascript
// src/test/integration/api/post.test.js
import { describe, it, expect, beforeEach } from "vitest";
import { request } from "./setup.js";
import { createTestUser, createTestPost, generateToken } from "../helpers/testUtils.js";

describe("Post API", () => {
  let author;
  let authorToken;
  let otherUser;
  let otherToken;

  beforeEach(async () => {
    const authorData = await createTestUser({ email: "author@example.com" });
    author = authorData;
    authorToken = generateToken(authorData);

    const otherData = await createTestUser({ email: "other@example.com" });
    otherUser = otherData;
    otherToken = generateToken(otherData);
  });

  describe("POST /api/posts", () => {
    it("登录用户应该能创建文章", async () => {
      const postData = {
        title: "我的第一篇文章",
        content: "这是文章内容...",
        tags: ["JavaScript", "React"],
      };

      const response = await request
        .post("/api/posts")
        .set("Authorization", `Bearer ${authorToken}`)
        .send(postData)
        .expect(201);

      expect(response.body.data.title).toBe(postData.title);
      expect(response.body.data.slug).toBe("wo-de-di-yi-pian-wen-zhang");
      expect(response.body.data.author._id).toBe(author.id);
    });

    it("未登录应该返回 401", async () => {
      await request
        .post("/api/posts")
        .send({ title: "Test", content: "Content" })
        .expect(401);
    });

    it("标题为空应该返回 400", async () => {
      await request
        .post("/api/posts")
        .set("Authorization", `Bearer ${authorToken}`)
        .send({ title: "", content: "Content" })
        .expect(400);
    });
  });

  describe("GET /api/posts", () => {
    it("应该返回已发布的文章列表", async () => {
      const publishedPost = await createTestPost({
        author: author.id,
        isPublished: true,
      });
      await createTestPost({
        author: author.id,
        isPublished: false,
      });

      const response = await request.get("/api/posts").expect(200);

      expect(response.body.data).toHaveLength(1);
      expect(response.body.data[0]._id).toBe(publishedPost.id);
    });

    it("应该支持分页", async () => {
      for (let i = 0; i < 15; i++) {
        await createTestPost({
          author: author.id,
          isPublished: true,
        });
      }

      const response = await request
        .get("/api/posts?page=1&limit=10")
        .expect(200);

      expect(response.body.data).toHaveLength(10);
      expect(response.body.pagination.total).toBe(15);
      expect(response.body.pagination.page).toBe(1);
    });

    it("应该能按标签筛选", async () => {
      await createTestPost({
        author: author.id,
        isPublished: true,
        tags: ["React"],
      });
      await createTestPost({
        author: author.id,
        isPublished: true,
        tags: ["Vue"],
      });

      const response = await request
        .get("/api/posts?tag=React")
        .expect(200);

      expect(response.body.data).toHaveLength(1);
      expect(response.body.data[0].tags).toContain("React");
    });
  });

  describe("GET /api/posts/:slug", () => {
    it("应该能获取已发布文章详情", async () => {
      const post = await createTestPost({
        author: author.id,
        isPublished: true,
        title: "测试文章",
      });

      const response = await request
        .get(`/api/posts/${post.slug}`)
        .expect(200);

      expect(response.body.data.title).toBe("测试文章");
    });

    it("未发布的文章对其他人不可见", async () => {
      const post = await createTestPost({
        author: author.id,
        isPublished: false,
      });

      await request.get(`/api/posts/${post.slug}`).expect(404);
    });

    it("作者应该能查看自己未发布的文章", async () => {
      const post = await createTestPost({
        author: author.id,
        isPublished: false,
      });

      await request
        .get(`/api/posts/${post.slug}`)
        .set("Authorization", `Bearer ${authorToken}`)
        .expect(200);
    });
  });

  describe("DELETE /api/posts/:id", () => {
    it("作者应该能删除自己的文章", async () => {
      const post = await createTestPost({ author: author.id });

      await request
        .delete(`/api/posts/${post.id}`)
        .set("Authorization", `Bearer ${authorToken}`)
        .expect(200);
    });

    it("非作者不能删除他人文章", async () => {
      const post = await createTestPost({ author: author.id });

      await request
        .delete(`/api/posts/${post.id}`)
        .set("Authorization", `Bearer ${otherToken}`)
        .expect(403);
    });
  });
});

数据库集成测试

MongoDB 测试

javascript
// src/test/helpers/mongodb.js
import mongoose from "mongoose";
import { MongoMemoryServer } from "mongodb-memory-server";

let mongoServer;

beforeAll(async () => {
  mongoServer = await MongoMemoryServer.create();
  const mongoUri = mongoServer.getUri();
  await mongoose.connect(mongoUri);
});

afterAll(async () => {
  await mongoose.disconnect();
  await mongoServer.stop();
});

afterEach(async () => {
  const collections = mongoose.connection.collections;
  for (const key in collections) {
    await collections[key].deleteMany({});
  }
});

PostgreSQL 测试

javascript
// src/test/helpers/postgres.js
import { Pool } from "pg";
import { exec } from "child_process";
import { promisify } from "util";

const execAsync = promisify(exec);

const pool = new Pool({
  host: "localhost",
  port: 5432,
  database: "test_db",
  user: "test",
  password: "test",
});

beforeAll(async () => {
  // 运行迁移
  await execAsync("npm run db:migrate:test");
});

afterAll(async () => {
  await pool.end();
});

afterEach(async () => {
  // 清理所有表
  await pool.query(`
    TRUNCATE TABLE users, posts, comments, tags
    RESTART IDENTITY CASCADE;
  `);
});

export { pool };

测试辅助工具

测试数据工厂

javascript
// src/test/helpers/factories.js
import { faker } from "@faker-js/faker";
import bcrypt from "bcryptjs";

// 用户工厂
export function createUserFactory(overrides = {}) {
  return {
    username: faker.internet.username(),
    email: faker.internet.email().toLowerCase(),
    password: "Password123!",
    avatar: faker.image.avatar(),
    bio: faker.lorem.sentence(),
    ...overrides,
  };
}

// 文章工厂
export function createPostFactory(overrides = {}) {
  return {
    title: faker.lorem.sentence(),
    slug: faker.lorem.slug(),
    content: faker.lorem.paragraphs(3),
    excerpt: faker.lorem.paragraph(),
    coverImage: faker.image.url(),
    tags: faker.random.arrayElements(["JavaScript", "React", "Vue", "Node.js", "Python"]),
    isPublished: faker.datatype.boolean(0.7),
    publishedAt: faker.datatype.boolean(0.7) ? faker.date.recent() : null,
    ...overrides,
  };
}

// 评论工厂
export function createCommentFactory(overrides = {}) {
  return {
    content: faker.lorem.sentence(),
    ...overrides,
  };
}

测试工具函数

javascript
// src/test/helpers/testUtils.js
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import User from "../../models/User.js";
import Post from "../../models/Post.js";
import {
  createUserFactory,
  createPostFactory,
} from "./factories.js";

export async function createTestUser(overrides = {}) {
  const userData = createUserFactory(overrides);

  if (userData.password) {
    const hashedPassword = await bcrypt.hash(userData.password, 10);
    userData.password = hashedPassword;
  }

  const user = await User.create(userData);
  return user;
}

export async function createTestPost(overrides = {}) {
  const postData = createPostFactory(overrides);
  const post = await Post.create(postData);
  return post;
}

export function generateToken(user) {
  return jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET || "test-secret",
    { expiresIn: "1h" }
  );
}

API 认证测试

javascript
// src/test/integration/api/auth.test.js
import { describe, it, expect } from "vitest";
import { request } from "./setup.js";
import { createTestUser, generateToken } from "../helpers/testUtils.js";

describe("Authentication", () => {
  describe("Token Validation", () => {
    it("应该拒绝过期的 Token", async () => {
      const user = await createTestUser();
      const expiredToken = jwt.sign(
        { userId: user.id },
        process.env.JWT_SECRET,
        { expiresIn: "-1h" }
      );

      await request
        .get("/api/users/me")
        .set("Authorization", `Bearer ${expiredToken}`)
        .expect(401);
    });

    it("应该拒绝格式错误的 Token", async () => {
      await request
        .get("/api/users/me")
        .set("Authorization", "Bearer not-a-valid-token")
        .expect(401);
    });

    it("多个 Token 应该都能正常工作", async () => {
      const user1 = await createTestUser({ email: "user1@test.com" });
      const user2 = await createTestUser({ email: "user2@test.com" });

      const token1 = generateToken(user1);
      const token2 = generateToken(user2);

      const [res1, res2] = await Promise.all([
        request
          .get("/api/users/me")
          .set("Authorization", `Bearer ${token1}`)
          .expect(200),
        request
          .get("/api/users/me")
          .set("Authorization", `Bearer ${token2}`)
          .expect(200),
      ]);

      expect(res1.body.data.email).toBe("user1@test.com");
      expect(res2.body.data.email).toBe("user2@test.com");
    });
  });
});

测试环境

集成测试应该在独立的测试环境运行,使用内存数据库或测试数据库副本,避免污染生产数据。

总结

使用 Claude Code 编写集成测试:

  • 使用 Supertest 测试 API 端点
  • 使用内存数据库进行隔离测试
  • 创建测试数据工厂简化测试
  • 测试正常流程和异常流程
  • 验证认证和授权机制
  • 确保测试的独立性和可重复性