实战案例分析(三)- 测试框架实践
1. 测试基础概念介绍
测试类型对比表
测试类型 | 测试范围 | 测试目标 | 依赖处理 |
---|---|---|---|
单元测试 | 单个函数/类 | 验证最小可测试单元 | 使用Mock替代依赖 |
集成测试 | 多个模块交互 | 验证模块间协作 | 使用真实依赖或集成环境 |
端到端测试 | 整个应用流程 | 验证完整业务流程 | 使用完整的应用环境 |
让我们通过一个实际的API开发案例来学习如何使用Cursor AI进行测试。
2. 案例:用户认证API测试实现
2.1 API实现代码
python">// src/services/AuthService.ts
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { UserRepository } from '../repositories/UserRepository';export interface IUserCredentials {email: string;password: string;
}export interface IUser {id: string;email: string;password: string;createdAt: Date;
}export class AuthService {private userRepo: UserRepository;private readonly JWT_SECRET = process.env.JWT_SECRET || 'default_secret';private readonly SALT_ROUNDS = 10;constructor(userRepo: UserRepository) {this.userRepo = userRepo;}async register(credentials: IUserCredentials): Promise<Omit<IUser, 'password'>> {// Validate inputif (!this.isValidEmail(credentials.email)) {throw new Error('Invalid email format');}if (!this.isValidPassword(credentials.password)) {throw new Error('Password must be at least 8 characters long');}// Check if user existsconst existingUser = await this.userRepo.findByEmail(credentials.email);if (existingUser) {throw new Error('Email already registered');}// Hash passwordconst hashedPassword = await bcrypt.hash(credentials.password, this.SALT_ROUNDS);// Create userconst user = await this.userRepo.create({email: credentials.email,password: hashedPassword});// Remove password from responseconst { password, ...userWithoutPassword } = user;return userWithoutPassword;}async login(credentials: IUserCredentials): Promise<string> {// Find userconst user = await this.userRepo.findByEmail(credentials.email);if (!user) {throw new Error('User not found');}// Verify passwordconst isValid = await bcrypt.compare(credentials.password, user.password);if (!isValid) {throw new Error('Invalid password');}// Generate tokenreturn jwt.sign({ userId: user.id, email: user.email },this.JWT_SECRET,{ expiresIn: '24h' });}private isValidEmail(email: string): boolean {const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;return emailRegex.test(email);}private isValidPassword(password: string): boolean {return password.length >= 8;}
}
2.2 单元测试实现
python">// src/tests/unit/AuthService.test.ts
import { AuthService, IUserCredentials, IUser } from '../../services/AuthService';
import { UserRepository } from '../../repositories/UserRepository';// Mock UserRepository
jest.mock('../../repositories/UserRepository');describe('AuthService', () => {let authService: AuthService;let mockUserRepo: jest.Mocked<UserRepository>;beforeEach(() => {// Clear all mocks before each testjest.clearAllMocks();mockUserRepo = new UserRepository() as jest.Mocked<UserRepository>;authService = new AuthService(mockUserRepo);});describe('register', () => {const validCredentials: IUserCredentials = {email: 'test@example.com',password: 'password123'};it('should successfully register a new user', async () => {// ArrangemockUserRepo.findByEmail.mockResolvedValue(null);mockUserRepo.create.mockResolvedValue({id: '1',email: validCredentials.email,password: 'hashed_password',createdAt: new Date()});// Actconst result = await authService.register(validCredentials);// Assertexpect(result).toHaveProperty('id');expect(result.email).toBe(validCredentials.email);expect(result).not.toHaveProperty('password');expect(mockUserRepo.create).toHaveBeenCalledTimes(1);});it('should throw error for invalid email format', async () => {// Arrangeconst invalidCredentials = {email: 'invalid-email',password: 'password123'};// Act & Assertawait expect(authService.register(invalidCredentials)).rejects.toThrow('Invalid email format');expect(mockUserRepo.create).not.toHaveBeenCalled();});it('should throw error for short password', async () => {// Arrangeconst invalidCredentials = {email: 'test@example.com',password: 'short'};// Act & Assertawait expect(authService.register(invalidCredentials)).rejects.toThrow('Password must be at least 8 characters long');expect(mockUserRepo.create).not.toHaveBeenCalled();});it('should throw error for existing email', async () => {// ArrangemockUserRepo.findByEmail.mockResolvedValue({id: '1',email: validCredentials.email,password: 'hashed_password',createdAt: new Date()});// Act & Assertawait expect(authService.register(validCredentials)).rejects.toThrow('Email already registered');expect(mockUserRepo.create).not.toHaveBeenCalled();});});describe('login', () => {const validCredentials: IUserCredentials = {email: 'test@example.com',password: 'password123'};it('should return JWT token for valid credentials', async () => {// ArrangemockUserRepo.findByEmail.mockResolvedValue({id: '1',email: validCredentials.email,password: await bcrypt.hash(validCredentials.password, 10),createdAt: new Date()});// Actconst token = await authService.login(validCredentials);// Assertexpect(typeof token).toBe('string');expect(token.split('.')).toHaveLength(3); // JWT format validation});it('should throw error for non-existent user', async () => {// ArrangemockUserRepo.findByEmail.mockResolvedValue(null);// Act & Assertawait expect(authService.login(validCredentials)).rejects.toThrow('User not found');});it('should throw error for invalid password', async () => {// ArrangemockUserRepo.findByEmail.mockResolvedValue({id: '1',email: validCredentials.email,password: await bcrypt.hash('different_password', 10),createdAt: new Date()});// Act & Assertawait expect(authService.login(validCredentials)).rejects.toThrow('Invalid password');});});
});
2.3 集成测试实现
python">// src/tests/integration/auth.integration.test.ts
import request from 'supertest';
import { app } from '../../app';
import { Database } from '../../database';
import { UserRepository } from '../../repositories/UserRepository';describe('Authentication API Integration Tests', () => {let db: Database;let userRepo: UserRepository;beforeAll(async () => {// Initialize test databasedb = new Database({host: process.env.TEST_DB_HOST,database: process.env.TEST_DB_NAME});await db.connect();userRepo = new UserRepository(db);});afterAll(async () => {// Cleanupawait db.disconnect();});beforeEach(async () => {// Clear users table before each testawait db.query('TRUNCATE TABLE users CASCADE');});describe('POST /api/auth/register', () => {it('should successfully register a new user', async () => {const response = await request(app).post('/api/auth/register').send({email: 'test@example.com',password: 'password123'});expect(response.status).toBe(201);expect(response.body).toHaveProperty('id');expect(response.body.email).toBe('test@example.com');expect(response.body).not.toHaveProperty('password');// Verify user was created in databaseconst user = await userRepo.findByEmail('test@example.com');expect(user).toBeTruthy();expect(user?.email).toBe('test@example.com');});it('should not allow duplicate email registration', async () => {// First registrationawait request(app).post('/api/auth/register').send({email: 'test@example.com',password: 'password123'});// Attempt duplicate registrationconst response = await request(app).post('/api/auth/register').send({email: 'test@example.com',password: 'different_password'});expect(response.status).toBe(400);expect(response.body.error).toBe('Email already registered');});});describe('POST /api/auth/login', () => {beforeEach(async () => {// Create test userawait request(app).post('/api/auth/register').send({email: 'test@example.com',password: 'password123'});});it('should return JWT token for valid credentials', async () => {const response = await request(app).post('/api/auth/login').send({email: 'test@example.com',password: 'password123'});expect(response.status).toBe(200);expect(response.body).toHaveProperty('token');expect(typeof response.body.token).toBe('string');// Verify token is valid by using itconst protectedResponse = await request(app).get('/api/protected-route').set('Authorization', `Bearer ${response.body.token}`);expect(protectedResponse.status).toBe(200);});it('should reject invalid credentials', async () => {const response = await request(app).post('/api/auth/login').send({email: 'test@example.com',password: 'wrong_password'});expect(response.status).toBe(401);expect(response.body.error).toBe('Invalid password');});});
});
2.4 测试流程图
AI_370">3. 使用Cursor AI生成测试用例
3.1 测试描述生成器
继续完善测试用例生成器的实现:
python">interface ITestCase {description: string;input: any;expectedOutput: any;type: 'unit' | 'integration';setup?: string[];cleanup?: string[];
}class TestCaseGenerator {static generateAuthTestCases(): ITestCase[] {return [// 注册测试用例{description: "should successfully register new user with valid credentials",input: {email: "test@example.com",password: "validPassword123"},expectedOutput: {status: 201,hasId: true,hasEmail: true,noPassword: true},type: "unit"},{description: "should reject registration with invalid email",input: {email: "invalid-email",password: "validPassword123"},expectedOutput: {error: "Invalid email format"},type: "unit"},// 登录测试用例{description: "should successfully login with valid credentials",input: {email: "test@example.com",password: "validPassword123"},expectedOutput: {status: 200,hasToken: true},type: "integration",setup: ["create test user","verify user exists in database"],cleanup: ["remove test user"]}];}static generateTestCode(testCase: ITestCase): string {const { description, input, expectedOutput, type } = testCase;let testCode = "";if (type === "unit") {testCode = `it('${description}', async () => {const response = await authService.${input.password ? 'register' : 'login'}(${JSON.stringify(input)});${this.generateAssertions(expectedOutput)}});`;} else {testCode = `it('${description}', async () => {${testCase.setup?.map(step => `// ${step}`).join('\n')}const response = await request(app).post('/api/auth/${input.password ? 'register' : 'login'}').send(${JSON.stringify(input)});${this.generateAssertions(expectedOutput)}${testCase.cleanup?.map(step => `// ${step}`).join('\n')}});`;}return testCode;}private static generateAssertions(expectedOutput: any): string {const assertions = [];if (expectedOutput.status) {assertions.push(`expect(response.status).toBe(${expectedOutput.status});`);}if (expectedOutput.hasId) {assertions.push(`expect(response.body).toHaveProperty('id');`);}if (expectedOutput.hasEmail) {assertions.push(`expect(response.body).toHaveProperty('email');`);}if (expectedOutput.noPassword) {assertions.push(`expect(response.body).not.toHaveProperty('password');`);}if (expectedOutput.hasToken) {assertions.push(`expect(response.body).toHaveProperty('token');`);assertions.push(`expect(typeof response.body.token).toBe('string');`);}if (expectedOutput.error) {assertions.push(`expect(response.body.error).toBe('${expectedOutput.error}');`);}return assertions.join('\n');}
}
3.2 测试辅助工具
python">// src/tests/utils/TestHelper.ts
import { Database } from '../../database';
import jwt from 'jsonwebtoken';export class TestHelper {private db: Database;constructor(db: Database) {this.db = db;}async createTestUser(email: string, hashedPassword: string) {return await this.db.query('INSERT INTO users (email, password) VALUES ($1, $2) RETURNING *',[email, hashedPassword]);}async clearTestData() {await this.db.query('TRUNCATE TABLE users CASCADE');}generateTestToken(userId: string): string {return jwt.sign({ userId },process.env.JWT_SECRET || 'test_secret',{ expiresIn: '1h' });}async verifyTestToken(token: string) {return jwt.verify(token, process.env.JWT_SECRET || 'test_secret');}
}// src/tests/utils/MockGenerator.ts
export class MockGenerator {static createMockUser(overrides = {}) {return {id: 'test-user-id',email: 'test@example.com',password: 'hashed_password',createdAt: new Date(),...overrides};}static createMockRequest(overrides = {}) {return {body: {},headers: {},params: {},query: {},...overrides};}static createMockResponse() {const res: any = {};res.status = jest.fn().mockReturnValue(res);res.json = jest.fn().mockReturnValue(res);res.send = jest.fn().mockReturnValue(res);return res;}
}
4. 测试覆盖率分析
让我们添加测试覆盖率配置和分析工具:
python">// jest.config.js
module.exports = {preset: 'ts-jest',testEnvironment: 'node',roots: ['<rootDir>/src'],collectCoverage: true,coverageDirectory: 'coverage',coverageReporters: ['text', 'lcov', 'clover'],coverageThreshold: {global: {branches: 80,functions: 80,lines: 80,statements: 80}},collectCoverageFrom: ['src/**/*.{ts,tsx}','!src/**/*.d.ts','!src/**/index.ts','!src/types/**/*']
};// package.json scripts
{"scripts": {"test": "jest","test:watch": "jest --watch","test:coverage": "jest --coverage","test:ci": "jest --ci --coverage --reporters=default --reporters=jest-junit"}
}
5. CI/CD集成测试配置
python">// .github/workflows/test.yml
name: Run Testson:push:branches: [ main ]pull_request:branches: [ main ]jobs:test:runs-on: ubuntu-latestservices:postgres:image: postgres:13env:POSTGRES_USER: testPOSTGRES_PASSWORD: testPOSTGRES_DB: testdbports:- 5432:5432options: >---health-cmd pg_isready--health-interval 10s--health-timeout 5s--health-retries 5steps:- uses: actions/checkout@v2- name: Use Node.jsuses: actions/setup-node@v2with:node-version: '16.x'cache: 'npm'- name: Install dependenciesrun: npm ci- name: Run testsrun: npm run test:cienv:TEST_DB_HOST: localhostTEST_DB_PORT: 5432TEST_DB_USER: testTEST_DB_PASSWORD: testTEST_DB_NAME: testdbJWT_SECRET: test_secret- name: Upload coverage to Codecovuses: codecov/codecov-action@v3with:token: ${{ secrets.CODECOV_TOKEN }}files: ./coverage/lcov.infofail_ci_if_error: true
6. 最佳实践总结
-
测试原则
- 遵循 AAA(Arrange-Act-Assert)模式
- 每个测试只测试一个概念
- 使用有意义的测试描述
- 保持测试代码的简洁和可维护性
-
测试策略
- 单元测试:最小可测试单元 - 集成测试:模块间交互 - 端到端测试:完整业务流程
-
Mock策略
- 只Mock必要的依赖
- 保持Mock的简单性
- 避免过度Mock
-
测试覆盖率目标
- 分支覆盖率:80% - 行覆盖率:80% - 函数覆盖率:80%
通过本实战案例,学习了如何使用Cursor AI进行全面的测试实践。从基本的单元测试到复杂的集成测试,再到CI/CD环境中的自动化测试,我们掌握了现代软件测试的关键技术和最佳实践。记住,好的测试不仅能够保证代码质量,还能提供清晰的代码文档和使用示例。
在实际项目中,要根据具体需求和资源情况选择合适的测试策略,并持续优化测试流程,以达到高效率和高质量的平衡。同时,要注意保持测试代码的可维护性,避免测试代码本身成为负担。
怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!