测试自动化实战
上个月我接手了一个没有任何测试的遗留项目。按照传统做法,写测试至少要一周,但用Claude Code,一天就搞定了单元测试、集成测试、E2E测试全套。
今天分享这个实战案例,看看Claude Code怎么让测试覆盖率从0提升到90%+。
项目背景
遗留项目现状
一个在线商城系统的后端API,代码量约5000行:
- 没有测试: 完全没有单元测试、集成测试
- Bug频出: 每次上线都引入新问题
- 重构困难: 不敢改动旧代码,怕引入Bug
- 文档缺失: 很多函数用途不明确
技术栈
- 后端: Node.js + Express.js
- 数据库: MongoDB + Mongoose
- 测试框架: Jest + Supertest
- E2E测试: Playwright
目标
- 单元测试: 覆盖所有核心业务逻辑
- 集成测试: 测试API端点
- E2E测试: 关键用户流程
- 测试覆盖率: 达到90%以上
- CI/CD集成: 自动化测试流程
准备工作
安装测试依赖
首先让Claude Code帮你设置测试环境:
为这个Express项目配置完整的测试环境:
- Jest作为测试框架
- Supertest用于API测试
- Istanbul生成覆盖率报告
- Playwright用于E2E测试
创建测试配置文件和目录结构。
Claude Code会自动生成:
package.json (添加测试脚本):
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:ci": "jest --ci --coverage --maxWorkers=2",
"test:integration": "jest --testPathPattern=integration",
"test:e2e": "playwright test",
"test:all": "npm run test && npm run test:integration && npm run test:e2e"
},
"devDependencies": {
"@playwright/test": "^1.40.0",
"jest": "^29.7.0",
"supertest": "^6.3.3",
"mongodb-memory-server": "^9.1.3"
},
"jest": {
"testEnvironment": "node",
"coveragePathIgnorePatterns": ["/node_modules/"],
"testMatch": [
"**/__tests__/**/*.test.js",
"**/?(*.)+(spec|test).js"
],
"collectCoverageFrom": [
"src/**/*.js",
"!src/server.js",
"!src/config/**"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
项目结构
Claude Code会组织成这样的结构:
project/
├── src/
│ ├── models/
│ ├── controllers/
│ ├── services/
│ ├── routes/
│ └── utils/
├── __tests__/
│ ├── unit/
│ │ ├── models/
│ │ ├── services/
│ │ └── utils/
│ ├── integration/
│ │ └── api/
│ └── fixtures/
├── tests-e2e/
│ ├── specs/
│ └── helpers/
├── jest.setup.js
└── playwright.config.js
单元测试生成
测试数据模型
先从最简单的模型测试开始:
为Product模型生成完整的单元测试:
- 字段验证测试
- 必填字段测试
- 唯一性测试
- 自定义方法测试
- 虚拟字段测试
src/models/Product.js (被测试的模型):
const mongoose = require('mongoose');
const { Decimal128 } = require('bson');
const productSchema = new mongoose.Schema({
name: {
type: String,
required: [true, 'Product name is required'],
trim: true,
maxlength: [200, 'Name cannot exceed 200 characters'],
},
description: {
type: String,
maxlength: [2000, 'Description cannot exceed 2000 characters'],
},
price: {
type: Decimal128,
required: [true, 'Price is required'],
min: [0, 'Price cannot be negative'],
},
category: {
type: String,
required: true,
enum: ['electronics', 'clothing', 'books', 'home', 'sports'],
},
stock: {
type: Number,
default: 0,
min: [0, 'Stock cannot be negative'],
},
isActive: {
type: Boolean,
default: true,
},
tags: [{
type: String,
trim: true,
}],
ratings: [{
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
},
score: {
type: Number,
required: true,
min: 1,
max: 5,
},
comment: String,
}],
}, {
timestamps: true,
});
// 虚拟字段: 平均评分
productSchema.virtual('averageRating').get(function() {
if (this.ratings.length === 0) return 0;
const sum = this.ratings.reduce((acc, rating) => acc + rating.score, 0);
return (sum / this.ratings.length).toFixed(1);
});
// 实例方法: 减少库存
productSchema.methods.reduceStock = function(quantity) {
if (this.stock < quantity) {
throw new Error('Insufficient stock');
}
this.stock -= quantity;
return this.save();
};
// 实例方法: 增加库存
productSchema.methods.increaseStock = function(quantity) {
this.stock += quantity;
return this.save();
};
// 静态方法: 查找活跃产品
productSchema.statics.findActive = function() {
return this.find({ isActive: true });
};
// 静态方法: 按类别查找
productSchema.statics.findByCategory = function(category) {
return this.find({ category, isActive: true });
};
module.exports = mongoose.model('Product', productSchema);
Claude Code生成的测试文件:
tests/unit/models/Product.test.js:
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const Product = require('../../../src/models/Product');
describe('Product Model', () => {
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterEach(async () => {
await Product.deleteMany({});
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
describe('字段验证', () => {
test('应该成功创建一个有效产品', async () => {
const productData = {
name: 'iPhone 15 Pro',
description: 'Latest iPhone model',
price: '999.99',
category: 'electronics',
stock: 50,
};
const product = await Product.create(productData);
expect(product.name).toBe(productData.name);
expect(product.description).toBe(productData.description);
expect(product.price.toString()).toBe(productData.price);
expect(product.category).toBe(productData.category);
expect(product.stock).toBe(productData.stock);
expect(product.isActive).toBe(true);
});
test('应该缺少name字段时验证失败', async () => {
const productData = {
description: 'No name product',
price: '10.00',
category: 'electronics',
};
await expect(Product.create(productData)).rejects.toThrow();
});
test('应该缺少price字段时验证失败', async () => {
const productData = {
name: 'Test Product',
description: 'Test description',
category: 'electronics',
};
await expect(Product.create(productData)).rejects.toThrow();
});
test('应该无效类别时验证失败', async () => {
const productData = {
name: 'Test Product',
price: '10.00',
category: 'invalid-category',
};
await expect(Product.create(productData)).rejects.toThrow();
});
test('应该负价格时验证失败', async () => {
const productData = {
name: 'Test Product',
price: '-10.00',
category: 'electronics',
};
await expect(Product.create(productData)).rejects.toThrow();
});
test('应该负库存时验证失败', async () => {
const productData = {
name: 'Test Product',
price: '10.00',
category: 'electronics',
stock: -5,
};
await expect(Product.create(productData)).rejects.toThrow();
});
test('应该name超过最大长度时验证失败', async () => {
const productData = {
name: 'a'.repeat(201),
price: '10.00',
category: 'electronics',
};
await expect(Product.create(productData)).rejects.toThrow();
});
});
describe('虚拟字段: averageRating', () => {
test('应该返回0当没有评分时', () => {
const product = new Product({
name: 'Test Product',
price: '10.00',
category: 'electronics',
ratings: [],
});
expect(product.averageRating).toBe(0);
});
test('应该计算平均评分正确', () => {
const product = new Product({
name: 'Test Product',
price: '10.00',
category: 'electronics',
ratings: [
{ score: 4, comment: 'Good' },
{ score: 5, comment: 'Excellent' },
{ score: 3, comment: 'Okay' },
],
});
expect(product.averageRating).toBe('4.0');
});
test('应该处理单个评分', () => {
const product = new Product({
name: 'Test Product',
price: '10.00',
category: 'electronics',
ratings: [{ score: 5, comment: 'Perfect' }],
});
expect(product.averageRating).toBe('5.0');
});
});
describe('实例方法: reduceStock', () => {
test('应该成功减少库存', async () => {
const product = await Product.create({
name: 'Test Product',
price: '10.00',
category: 'electronics',
stock: 100,
});
await product.reduceStock(10);
expect(product.stock).toBe(90);
});
test('应该库存不足时抛出错误', async () => {
const product = await Product.create({
name: 'Test Product',
price: '10.00',
category: 'electronics',
stock: 5,
});
await expect(product.reduceStock(10)).rejects.toThrow('Insufficient stock');
});
test('应该完全消耗库存', async () => {
const product = await Product.create({
name: 'Test Product',
price: '10.00',
category: 'electronics',
stock: 10,
});
await product.reduceStock(10);
expect(product.stock).toBe(0);
});
});
describe('实例方法: increaseStock', () => {
test('应该成功增加库存', async () => {
const product = await Product.create({
name: 'Test Product',
price: '10.00',
category: 'electronics',
stock: 50,
});
await product.increaseStock(25);
expect(product.stock).toBe(75);
});
test('应该添加库存到零库存产品', async () => {
const product = await Product.create({
name: 'Test Product',
price: '10.00',
category: 'electronics',
stock: 0,
});
await product.increaseStock(100);
expect(product.stock).toBe(100);
});
});
describe('静态方法: findActive', () => {
beforeEach(async () => {
await Product.create([
{ name: 'Active Product 1', price: '10.00', category: 'electronics', isActive: true },
{ name: 'Active Product 2', price: '20.00', category: 'electronics', isActive: true },
{ name: 'Inactive Product', price: '30.00', category: 'electronics', isActive: false },
]);
});
test('应该只返回活跃产品', async () => {
const activeProducts = await Product.findActive();
expect(activeProducts).toHaveLength(2);
expect(activeProducts.every(p => p.isActive)).toBe(true);
});
});
describe('静态方法: findByCategory', () => {
beforeEach(async () => {
await Product.create([
{ name: 'iPhone', price: '999.00', category: 'electronics', isActive: true },
{ name: 'MacBook', price: '1999.00', category: 'electronics', isActive: true },
{ name: 'T-Shirt', price: '19.99', category: 'clothing', isActive: true },
{ name: 'Inactive Electronics', price: '100.00', category: 'electronics', isActive: false },
]);
});
test('应该返回指定类别的活跃产品', async () => {
const electronics = await Product.findByCategory('electronics');
expect(electronics).toHaveLength(2);
expect(electronics.every(p => p.category === 'electronics')).toBe(true);
expect(electronics.every(p => p.isActive)).toBe(true);
});
test('应该返回空数组当没有该类别产品', async () => {
const books = await Product.findByCategory('books');
expect(books).toHaveLength(0);
});
});
describe('标签功能', () => {
test('应该成功创建带标签的产品', async () => {
const product = await Product.create({
name: 'Test Product',
price: '10.00',
category: 'electronics',
tags: ['new', 'featured', 'sale'],
});
expect(product.tags).toHaveLength(3);
expect(product.tags).toContain('new');
expect(product.tags).toContain('featured');
expect(product.tags).toContain('sale');
});
test('应该修剪标签中的空白', async () => {
const product = await Product.create({
name: 'Test Product',
price: '10.00',
category: 'electronics',
tags: [' new ', 'featured', ' sale '],
});
expect(product.tags[0]).toBe('new');
expect(product.tags[2]).toBe('sale');
});
});
});
测试业务逻辑服务
继续测试服务层:
为OrderService生成完整的单元测试:
- 创建订单逻辑
- 库存验证
- 价格计算
- 折扣应用
- 错误处理
- 边界条件
src/services/orderService.js:
const Product = require('../models/Product');
const Order = require('../models/Order');
class OrderService {
// 创建订单
async createOrder(userId, items, discountCode = null) {
// 验证输入
if (!items || items.length === 0) {
throw new Error('Order must contain at least one item');
}
// 计算总价和验证库存
let totalAmount = 0;
const orderItems = [];
for (const item of items) {
const product = await Product.findById(item.productId);
if (!product) {
throw new Error(`Product ${item.productId} not found`);
}
if (!product.isActive) {
throw new Error(`Product ${product.name} is not available`);
}
if (product.stock < item.quantity) {
throw new Error(`Insufficient stock for ${product.name}`);
}
const itemTotal = parseFloat(product.price.toString()) * item.quantity;
totalAmount += itemTotal;
orderItems.push({
product: product._id,
name: product.name,
quantity: item.quantity,
price: product.price,
subtotal: itemTotal,
});
}
// 应用折扣
let discount = 0;
if (discountCode) {
discount = await this.applyDiscount(discountCode, totalAmount);
totalAmount -= discount;
}
// 创建订单
const order = await Order.create({
user: userId,
items: orderItems,
totalAmount,
discount,
status: 'pending',
});
// 减少库存
for (const item of items) {
const product = await Product.findById(item.productId);
await product.reduceStock(item.quantity);
}
return order;
}
// 应 用折扣码
async applyDiscount(code, totalAmount) {
const discounts = {
'SAVE10': 0.10, // 10% off
'SAVE20': 0.20, // 20% off
'FIRST50': 50, // $50 off
};
const discount = discounts[code];
if (!discount) {
throw new Error('Invalid discount code');
}
if (typeof discount === 'number' && discount < 1) {
// 百分比折扣
return totalAmount * discount;
} else {
// 固定金额折扣
return Math.min(discount, totalAmount);
}
}
// 取消订单
async cancelOrder(orderId, userId) {
const order = await Order.findOne({ _id: orderId, user: userId });
if (!order) {
throw new Error('Order not found');
}
if (order.status === 'cancelled' || order.status === 'delivered') {
throw new Error(`Cannot cancel order with status: ${order.status}`);
}
// 恢复库存
for (const item of order.items) {
const product = await Product.findById(item.product);
if (product) {
await product.increaseStock(item.quantity);
}
}
order.status = 'cancelled';
order.cancelledAt = Date.now();
await order.save();
return order;
}
// 获取订单状态
async getOrderStatus(orderId, userId) {
const order = await Order.findOne({ _id: orderId, user: userId });
if (!order) {
throw new Error('Order not found');
}
return {
id: order._id,
status: order.status,
totalAmount: order.totalAmount,
createdAt: order.createdAt,
items: order.items,
};
}
}
module.exports = new OrderService();
tests/unit/services/orderService.test.js:
const OrderService = require('../../../src/services/orderService');
const Product = require('../../../src/models/Product');
const Order = require('../../../src/models/Order');
const { MongoMemoryServer } = require('mongodb-memory-server');
const mongoose = require('mongoose');
describe('OrderService', () => {
let mongoServer;
let userId;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
userId = new mongoose.Types.ObjectId();
});
afterEach(async () => {
await Product.deleteMany({});
await Order.deleteMany({});
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
describe('createOrder', () => {
let product1, product2;
beforeEach(async () => {
product1 = await Product.create({
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
});
product2 = await Product.create({
name: 'MacBook Pro',
price: '1999.00',
category: 'electronics',
stock: 20,
});
});
test('应该成功创建单产品订单', async () => {
const items = [{ productId: product1._id, quantity: 1 }];
const order = await OrderService.createOrder(userId, items);
expect(order).toBeDefined();
expect(order.items).toHaveLength(1);
expect(order.totalAmount.toString()).toBe('999');
expect(order.status).toBe('pending');
// 验证库存减少
const updatedProduct = await Product.findById(product1._id);
expect(updatedProduct.stock).toBe(49);
});
test('应该成功创建多产品订单', async () => {
const items = [
{ productId: product1._id, quantity: 2 },
{ productId: product2._id, quantity: 1 },
];
const order = await OrderService.createOrder(userId, items);
expect(order.items).toHaveLength(2);
const expectedTotal = 999 * 2 + 1999;
expect(order.totalAmount.toString()).toBe(expectedTotal.toString());
// 验证库存
const p1 = await Product.findById(product1._id);
const p2 = await Product.findById(product2._id);
expect(p1.stock).toBe(48);
expect(p2.stock).toBe(19);
});
test('应该空产品列表时抛出错误', async () => {
await expect(OrderService.createOrder(userId, [])).rejects.toThrow(
'Order must contain at least one item'
);
});
test('应该产品不存在时抛出错误', async () => {
const nonExistentId = new mongoose.Types.ObjectId();
const items = [{ productId: nonExistentId, quantity: 1 }];
await expect(OrderService.createOrder(userId, items)).rejects.toThrow(
'Product not found'
);
});
test('