测试自动化实战
上个月我接手了一个没有任何测试的遗留项目。按照传统做法,写测试至少要一周,但用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('应该产品不可用时抛出错误', async () => {
await Product.findByIdAndUpdate(product1._id, { isActive: false });
const items = [{ productId: product1._id, quantity: 1 }];
await expect(OrderService.createOrder(userId, items)).rejects.toThrow(
'is not available'
);
});
test('应该库存不足时抛出错误', async () => {
await Product.findByIdAndUpdate(product1._id, { stock: 2 });
const items = [{ productId: product1._id, quantity: 5 }];
await expect(OrderService.createOrder(userId, items)).rejects.toThrow(
'Insufficient stock'
);
});
});
describe('applyDiscount', () => {
test('应该应用10%百分比折扣', async () => {
const discount = await OrderService.applyDiscount('SAVE10', 1000);
expect(discount).toBe(100);
});
test('应该应用20%百分比折扣', async () => {
const discount = await OrderService.applyDiscount('SAVE20', 500);
expect(discount).toBe(100);
});
test('应该应用固定金额折扣', async () => {
const discount = await OrderService.applyDiscount('FIRST50', 1000);
expect(discount).toBe(50);
});
test('应该限制固定折扣不超过订单总额', async () => {
const discount = await OrderService.applyDiscount('FIRST50', 30);
expect(discount).toBe(30);
});
test('应该无效折扣码时抛出错误', async () => {
await expect(OrderService.applyDiscount('INVALID', 1000)).rejects.toThrow(
'Invalid discount code'
);
});
});
describe('createOrder with discount', () => {
let product1;
beforeEach(async () => {
product1 = await Product.create({
name: 'iPhone 15',
price: '1000.00',
category: 'electronics',
stock: 50,
});
});
test('应该应用百分比折扣到订单', async () => {
const items = [{ productId: product1._id, quantity: 1 }];
const order = await OrderService.createOrder(userId, items, 'SAVE20');
expect(order.discount).toBe(200);
expect(order.totalAmount.toString()).toBe('800');
});
test('应该应用固定金额折扣到订单', async () => {
const items = [{ productId: product1._id, quantity: 1 }];
const order = await OrderService.createOrder(userId, items, 'FIRST50');
expect(order.discount).toBe(50);
expect(order.totalAmount.toString()).toBe('950');
});
});
describe('cancelOrder', () => {
let order, product;
beforeEach(async () => {
product = await Product.create({
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
});
const items = [{ productId: product._id, quantity: 2 }];
order = await OrderService.createOrder(userId, items);
});
test('应该成功取消pending订单', async () => {
const cancelledOrder = await OrderService.cancelOrder(order._id, userId);
expect(cancelledOrder.status).toBe('cancelled');
expect(cancelledOrder.cancelledAt).toBeDefined();
// 验证库存恢复
const updatedProduct = await Product.findById(product._id);
expect(updatedProduct.stock).toBe(50);
});
test('应该订单不存在时抛出错误', async () => {
const fakeOrderId = new mongoose.Types.ObjectId();
await expect(OrderService.cancelOrder(fakeOrderId, userId)).rejects.toThrow(
'Order not found'
);
});
test('应该不允许取消已取消的订单', async () => {
await OrderService.cancelOrder(order._id, userId);
await expect(OrderService.cancelOrder(order._id, userId)).rejects.toThrow(
'Cannot cancel order'
);
});
});
describe('getOrderStatus', () => {
test('应该返回订单状态', async () => {
const product = await Product.create({
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
});
const items = [{ productId: product._id, quantity: 1 }];
const order = await OrderService.createOrder(userId, items);
const status = await OrderService.getOrderStatus(order._id, userId);
expect(status.id).toEqual(order._id);
expect(status.status).toBe('pending');
expect(status.totalAmount.toString()).toBe('999');
expect(status.items).toHaveLength(1);
expect(status.createdAt).toBeDefined();
});
test('应该订单不存在时抛出错误', async () => {
const fakeOrderId = new mongoose.Types.ObjectId();
await expect(OrderService.getOrderStatus(fakeOrderId, userId)).rejects.toThrow(
'Order not found'
);
});
});
});
运行单元测试
npm run test:coverage
输出示例:
PASS __tests__/unit/models/Product.test.js
Product Model
字段验证
✓ 应该成功创建一个有效产品 (45ms)
✓ 应该缺少name字段时验证失败 (12ms)
✓ 应该缺少price字段时验证失败 (8ms)
✓ 应该无效类别时验证失败 (9ms)
✓ 应该负价格时验证失败 (8ms)
✓ 应该负库存时验证失败 (7ms)
✓ 应该name超过最大长度时验证失败 (8ms)
虚拟字段: averageRating
✓ 应该返回0当没有评分时 (5ms)
✓ 应该计算平均评分正确 (6ms)
✓ 应该处理单个评分 (5ms)
实例方法: reduceStock
✓ 应该成功减少库存 (12ms)
✓ 应该库存不足时抛出错误 (8ms)
✓ 应该完全消耗库存 (9ms)
实例方法: increaseStock
✓ 应该成功增加库存 (10ms)
✓ 应该添加库存到零库存产品 (8ms)
静态方法: findActive
✓ 应该只返 回活跃产品 (15ms)
静态方法: findByCategory
✓ 应该返回指定类别的活跃产品 (12ms)
✓ 应该返回空数组当没有该类别产品 (8ms)
标签功能
✓ 应该成功创建带标签的产品 (9ms)
✓ 应该修剪标签中的空白 (7ms)
PASS __tests__/unit/services/orderService.test.js
OrderService
createOrder
✓ 应该成功创建单产品订单 (35ms)
✓ 应该成功创建多产品订单 (28ms)
✓ 应该空产品列表时抛出错误 (8ms)
✓ 应该产品不存在时抛出错误 (9ms)
✓ 应该产品不可用时抛出错误 (10ms)
✓ 应该库存不足时抛出错误 (11ms)
applyDiscount
✓ 应该应用10%百分比折扣 (5ms)
✓ 应该应用20%百分比折扣 (4ms)
✓ 应该应用固定金额折扣 (4ms)
✓ 应该限制固定折扣不超过订单总额 (4ms)
✓ 应该无效折扣码时抛出错误 (4ms)
createOrder with discount
✓ 应该应用百分比折扣到订单 (22ms)
✓ 应该应用固定金额折扣到订单 (20ms)
cancelOrder
✓ 应该成功取消pending订单 (18ms)
✓ 应该订单不存在时抛出错误 (7ms)
✓ 应该不允许取消已取消的订单 (12ms)
getOrderStatus
✓ 应该返回订单状态 (15ms)
✓ 应该订单不存在时抛出错误 (7ms)
--------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
--------------------|---------|----------|---------|---------|-------------------
All files | 92.15 | 88.45 | 95.12 | 92.45 |
src | 100 | 100 | 100 | 100 |
src/models | 95.32 | 92.15 | 100 | 95.45 |
Product.js | 95.32 | 92.15 | 100 | 95.45 | 145-148
src/services | 89.45 | 85.71 | 90.91 | 89.23 |
orderService.js | 89.45 | 85.71 | 90.91 | 89.23 | 78-82
--------------------|---------|----------|---------|---------|-------------------
Test Suites: 2 passed, 2 total
Tests: 40 passed, 40 total
集成测试
API端点测试
接下来测试完整的API流程:
为所有产品相关的API端点生成集成测试:
- POST /api/products (创建产品)
- GET /api/products (获取产品列表)
- GET /api/products/:id (获取单个产品)
- PUT /api/products/:id (更新产品)
- DELETE /api/products/:id (删除产品)
包括:
- 认证测试
- 授权测试
- 验证测试
- 错误处理测试
tests/integration/api/products.test.js:
const request = require('supertest');
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
const app = require('../../../src/app');
const User = require('../../../src/models/User');
const Product = require('../../../src/models/Product');
const generateToken = require('../../../src/utils/generateToken');
describe('Products API Integration Tests', () => {
let mongoServer;
let adminToken;
let userToken;
let adminUser;
let regularUser;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
// 创建管理员用户
adminUser = await User.create({
name: 'Admin User',
email: 'admin@test.com',
password: 'password123',
role: 'admin',
});
adminToken = generateToken(adminUser._id);
// 创建普通用户
regularUser = await User.create({
name: 'Regular User',
email: 'user@test.com',
password: 'password123',
role: 'user',
});
userToken = generateToken(regularUser._id);
});
afterEach(async () => {
await Product.deleteMany({});
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
describe('POST /api/products', () => {
const validProductData = {
name: 'iPhone 15 Pro',
description: 'Latest Apple smartphone',
price: '999.99',
category: 'electronics',
stock: 50,
tags: ['new', 'apple'],
};
test('应该管理员成功创建产品', async () => {
const response = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${adminToken}`)
.send(validProductData)
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.name).toBe(validProductData.name);
expect(response.body.data.price.toString()).toBe(validProductData.price);
// 验证数据库
const product = await Product.findById(response.body.data._id);
expect(product).toBeDefined();
expect(product.name).toBe(validProductData.name);
});
test('应该普通用户创建产品时返回403', async () => {
const response = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${userToken}`)
.send(validProductData)
.expect(403);
expect(response.body.success).toBe(false);
expect(response.body.message).toContain('not authorized');
});
test('应该未认证用户返回401', async () => {
const response = await request(app)
.post('/api/products')
.send(validProductData)
.expect(401);
expect(response.body.success).toBe(false);
});
test('应该缺少必填字段时返回400', async () => {
const invalidData = {
name: 'Test Product',
// 缺少 price 和 category
};
const response = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${adminToken}`)
.send(invalidData)
.expect(400);
expect(response.body.success).toBe(false);
});
test('应该无效价格时返回400', async () => {
const invalidData = {
...validProductData,
price: 'invalid',
};
const response = await request(app)
.post('/api/products')
.set('Authorization', `Bearer ${adminToken}`)
.send(invalidData)
.expect(400);
expect(response.body.success).toBe(false);
});
});
describe('GET /api/products', () => {
beforeEach(async () => {
await Product.create([
{
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
isActive: true,
},
{
name: 'MacBook Pro',
price: '1999.00',
category: 'electronics',
stock: 20,
isActive: true,
},
{
name: 'Inactive Product',
price: '100.00',
category: 'electronics',
stock: 10,
isActive: false,
},
]);
});
test('应该返回所有活跃产品', async () => {
const response = await request(app)
.get('/api/products')
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.products).toHaveLength(2);
expect(response.body.data.products.every(p => p.isActive)).toBe(true);
});
test('应该支持按类别过滤', async () => {
await Product.create({
name: 'T-Shirt',
price: '19.99',
category: 'clothing',
stock: 100,
isActive: true,
});
const response = await request(app)
.get('/api/products?category=electronics')
.expect(200);
expect(response.body.data.products).toHaveLength(2);
expect(response.body.data.products.every(p => p.category === 'electronics')).toBe(true);
});
test('应该支持分页', async () => {
// 创建更多产品
for (let i = 0; i < 15; i++) {
await Product.create({
name: `Product ${i}`,
price: '10.00',
category: 'electronics',
stock: 10,
isActive: true,
});
}
const response = await request(app)
.get('/api/products?page=1&limit=10')
.expect(200);
expect(response.body.data.products).toHaveLength(10);
expect(response.body.data.pagination).toBeDefined();
expect(response.body.data.pagination.totalPages).toBeGreaterThan(1);
});
test('应该支持搜索', async () => {
const response = await request(app)
.get('/api/products?search=iPhone')
.expect(200);
expect(response.body.data.products).toHaveLength(1);
expect(response.body.data.products[0].name).toContain('iPhone');
});
});
describe('GET /api/products/:id', () => {
test('应该返回有效产品', async () => {
const product = await Product.create({
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
});
const response = await request(app)
.get(`/api/products/${product._id}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data._id).toEqual(product._id.toString());
expect(response.body.data.name).toBe(product.name);
});
test('应该无效ID时返回404', async () => {
const fakeId = new mongoose.Types.ObjectId();
const response = await request(app)
.get(`/api/products/${fakeId}`)
.expect(404);
expect(response.body.success).toBe(false);
});
test('应该无效ID格式时返回400', async () => {
const response = await request(app)
.get('/api/products/invalid-id')
.expect(400);
expect(response.body.success).toBe(false);
});
});
describe('PUT /api/products/:id', () => {
test('应该管理员成功更新产品', async () => {
const product = await Product.create({
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
});
const updateData = {
name: 'iPhone 15 Pro',
price: '1099.00',
stock: 100,
};
const response = await request(app)
.put(`/api/products/${product._id}`)
.set('Authorization', `Bearer ${adminToken}`)
.send(updateData)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.name).toBe(updateData.name);
expect(response.body.data.price.toString()).toBe(updateData.price);
// 验证数据库
const updatedProduct = await Product.findById(product._id);
expect(updatedProduct.name).toBe(updateData.name);
expect(updatedProduct.stock).toBe(updateData.stock);
});
test('应该普通用户返回403', async () => {
const product = await Product.create({
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
});
const response = await request(app)
.put(`/api/products/${product._id}`)
.set('Authorization', `Bearer ${userToken}`)
.send({ name: 'Updated Name' })
.expect(403);
expect(response.body.success).toBe(false);
});
test('应该防止更新类别为无效值', async () => {
const product = await Product.create({
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
});
const response = await request(app)
.put(`/api/products/${product._id}`)
.set('Authorization', `Bearer ${adminToken}`)
.send({ category: 'invalid-category' })
.expect(400);
expect(response.body.success).toBe(false);
});
});
describe('DELETE /api/products/:id', () => {
test('应该管理员成功删除产品', async () => {
const product = await Product.create({
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
});
const response = await request(app)
.delete(`/api/products/${product._id}`)
.set('Authorization', `Bearer ${adminToken}`)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.message).toContain('deleted');
// 验证删除
const deletedProduct = await Product.findById(product._id);
expect(deletedProduct).toBeNull();
});
test('应该普通用户返回403', async () => {
const product = await Product.create({
name: 'iPhone 15',
price: '999.00',
category: 'electronics',
stock: 50,
});
const response = await request(app)
.delete(`/api/products/${product._id}`)
.set('Authorization', `Bearer ${userToken}`)
.expect(403);
expect(response.body.success).toBe(false);
// 验证产品未被删除
const productStillExists = await Product.findById(product._id);
expect(productStillExists).toBeDefined();
});
});
});