集成测试概述
集成测试验证多个模块协作的正确性。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 端点
- 使用内存数据库进行隔离测试
- 创建测试数据工厂简化测试
- 测试正常流程和异常流程
- 验证认证和授权机制
- 确保测试的独立性和可重复性