每日学习30分轻松掌握CursorAI:实战案例分析(三)- 测试框架实践

ops/2025/1/18 0:13:31/
aidu_pl">

实战案例分析(三)- 测试框架实践

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. 最佳实践总结

  1. 测试原则

    • 遵循 AAA(Arrange-Act-Assert)模式
    • 每个测试只测试一个概念
    • 使用有意义的测试描述
    • 保持测试代码的简洁和可维护性
  2. 测试策略

    - 单元测试:最小可测试单元
    - 集成测试:模块间交互
    - 端到端测试:完整业务流程
    
  3. Mock策略

    • 只Mock必要的依赖
    • 保持Mock的简单性
    • 避免过度Mock
  4. 测试覆盖率目标

    - 分支覆盖率:80%
    - 行覆盖率:80%
    - 函数覆盖率:80%
    

通过本实战案例,学习了如何使用Cursor AI进行全面的测试实践。从基本的单元测试到复杂的集成测试,再到CI/CD环境中的自动化测试,我们掌握了现代软件测试的关键技术和最佳实践。记住,好的测试不仅能够保证代码质量,还能提供清晰的代码文档和使用示例。

在实际项目中,要根据具体需求和资源情况选择合适的测试策略,并持续优化测试流程,以达到高效率和高质量的平衡。同时,要注意保持测试代码的可维护性,避免测试代码本身成为负担。


怎么样今天的内容还满意吗?再次感谢朋友们的观看,关注GZH:凡人的AI工具箱,回复666,送您价值199的AI大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!


http://www.ppmy.cn/ops/150958.html

相关文章

【绝对无坑】Mongodb获取集合的字段以及数据类型信息

Mongodb获取集合的字段以及数据类型信息 感觉很LOW的一个数据仓工具seatunel&#xff0c;竟然不能自动读取mongodb的表结构信息&#xff0c;需要手工创建。 然鹅&#xff0c;本人对mongodb也是新手&#xff0c;很多操作也不知所措&#xff0c;作为一个DBA&#xff0c;始终还是…

LLM - 大模型 ScallingLaws 的 CLM 和 MLM 中不同系数(PLM) 教程(2)

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/145188660 免责声明&#xff1a;本文来源于个人知识与公开资料&#xff0c;仅用于学术交流&#xff0c;欢迎讨论&#xff0c;不支持转载。 Scalin…

PHP政务招商系统

&#x1f680;政务招商系统&#xff1a;开启智慧招商新纪元 &#x1f527;这是一款由顶尖技术团队基于PHPUniapp框架精心锻造的招商神器&#xff0c;它将先进的数据可视化技术与高效的平台管理深度融合&#xff0c;开创了一个前所未有的可视化招商管理平台。在这个平台上&…

从0开始学习搭网站第二天

前言&#xff1a;今天比较惭愧&#xff0c;中午打铲吃了一把&#xff0c;看着也到钻二了&#xff0c;干脆顺手把这个赛季的大师上了&#xff0c;于是乎一直到网上才开始工作&#xff0c;同样&#xff0c;今天的学习内容大多来自mdn社区mdn 目录 怎么把文件上传到web服务器采用S…

Python学习(三)基础入门(数据类型、变量、条件判断、模式匹配、循环)

目录 一、第一个 Python 程序1.1 命令行模式、Python 交互模式1.2 Python的执行方式1.3 SyntaxError 语法错误1.4 输入和输出 二、Python 基础2.1 Python 语法2.2 数据类型1&#xff09;Number 数字2&#xff09;String 字符串3&#xff09;List 列表4&#xff09;Tuple 元组5&…

检测模型安全的更高级的方法

1. 查询依赖攻击&#xff08;Dependency Query Attack&#xff09; 定义&#xff1a; 利用模型对上下文或外部知识库的依赖&#xff0c;通过操纵这些外部依赖来引导模型输出敏感或错误的信息。 实现方式&#xff1a; 在知识库中插入伪造的信息&#xff0c;观察模型如何处理…

MySQL程序之:连接到服务器的命令选项

本节介绍大多数MySQL客户端程序支持的选项&#xff0c;这些选项控制客户端程序如何建立与服务器的连接、连接是否加密以及连接是否压缩。这些选项可以在命令行或选项文件中给出。 连接建立的命令选项 本节介绍控制客户端程序如何建立与服务器的连接的选项。 表6.4连接建立选…

【25考研】西南交通大学计算机复试重点及经验分享!

一、复试内容 上机考试&#xff1a;考试题型为编程上机考试&#xff0c;使用 C 语言&#xff0c;考试时长包括 15 分钟模拟考试和 120 分钟正式考试&#xff0c;考试内容涵盖顺序结构、选择结构、循环结构、数组、指针、字符串处理、函数、递归、结构体、动态存储、链表等知识点…