每日学习30分轻松掌握CursorAI:实战案例分析(三)- 测试框架实践
每日学习30分轻松掌握CursorAI:第十四篇 - 实战案例分析(三) - 测试框架实践如果文章对你有帮助,还请给个三连好评,感谢感谢!
·
实战案例分析(三)- 测试框架实践
1. 测试基础概念介绍
测试类型对比表
测试类型 | 测试范围 | 测试目标 | 依赖处理 |
---|---|---|---|
单元测试 | 单个函数/类 | 验证最小可测试单元 | 使用Mock替代依赖 |
集成测试 | 多个模块交互 | 验证模块间协作 | 使用真实依赖或集成环境 |
端到端测试 | 整个应用流程 | 验证完整业务流程 | 使用完整的应用环境 |
让我们通过一个实际的API开发案例来学习如何使用Cursor AI进行测试。
2. 案例:用户认证API测试实现
2.1 API实现代码
// 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 input
if (!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 exists
const existingUser = await this.userRepo.findByEmail(credentials.email);
if (existingUser) {
throw new Error('Email already registered');
}
// Hash password
const hashedPassword = await bcrypt.hash(credentials.password, this.SALT_ROUNDS);
// Create user
const user = await this.userRepo.create({
email: credentials.email,
password: hashedPassword
});
// Remove password from response
const { password, ...userWithoutPassword } = user;
return userWithoutPassword;
}
async login(credentials: IUserCredentials): Promise<string> {
// Find user
const user = await this.userRepo.findByEmail(credentials.email);
if (!user) {
throw new Error('User not found');
}
// Verify password
const isValid = await bcrypt.compare(credentials.password, user.password);
if (!isValid) {
throw new Error('Invalid password');
}
// Generate token
return 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 单元测试实现
// 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 test
jest.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 () => {
// Arrange
mockUserRepo.findByEmail.mockResolvedValue(null);
mockUserRepo.create.mockResolvedValue({
id: '1',
email: validCredentials.email,
password: 'hashed_password',
createdAt: new Date()
});
// Act
const result = await authService.register(validCredentials);
// Assert
expect(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 () => {
// Arrange
const invalidCredentials = {
email: 'invalid-email',
password: 'password123'
};
// Act & Assert
await expect(
authService.register(invalidCredentials)
).rejects.toThrow('Invalid email format');
expect(mockUserRepo.create).not.toHaveBeenCalled();
});
it('should throw error for short password', async () => {
// Arrange
const invalidCredentials = {
email: 'test@example.com',
password: 'short'
};
// Act & Assert
await 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 () => {
// Arrange
mockUserRepo.findByEmail.mockResolvedValue({
id: '1',
email: validCredentials.email,
password: 'hashed_password',
createdAt: new Date()
});
// Act & Assert
await 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 () => {
// Arrange
mockUserRepo.findByEmail.mockResolvedValue({
id: '1',
email: validCredentials.email,
password: await bcrypt.hash(validCredentials.password, 10),
createdAt: new Date()
});
// Act
const token = await authService.login(validCredentials);
// Assert
expect(typeof token).toBe('string');
expect(token.split('.')).toHaveLength(3); // JWT format validation
});
it('should throw error for non-existent user', async () => {
// Arrange
mockUserRepo.findByEmail.mockResolvedValue(null);
// Act & Assert
await expect(
authService.login(validCredentials)
).rejects.toThrow('User not found');
});
it('should throw error for invalid password', async () => {
// Arrange
mockUserRepo.findByEmail.mockResolvedValue({
id: '1',
email: validCredentials.email,
password: await bcrypt.hash('different_password', 10),
createdAt: new Date()
});
// Act & Assert
await expect(
authService.login(validCredentials)
).rejects.toThrow('Invalid password');
});
});
});
2.3 集成测试实现
// 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 database
db = new Database({
host: process.env.TEST_DB_HOST,
database: process.env.TEST_DB_NAME
});
await db.connect();
userRepo = new UserRepository(db);
});
afterAll(async () => {
// Cleanup
await db.disconnect();
});
beforeEach(async () => {
// Clear users table before each test
await 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 database
const 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 registration
await request(app)
.post('/api/auth/register')
.send({
email: 'test@example.com',
password: 'password123'
});
// Attempt duplicate registration
const 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 user
await 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 it
const 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 测试流程图
3. 使用Cursor AI生成测试用例
3.1 测试描述生成器
继续完善测试用例生成器的实现:
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 测试辅助工具
// 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. 测试覆盖率分析
让我们添加测试覆盖率配置和分析工具:
// 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集成测试配置
// .github/workflows/test.yml
name: Run Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:13
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '16.x'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test:ci
env:
TEST_DB_HOST: localhost
TEST_DB_PORT: 5432
TEST_DB_USER: test
TEST_DB_PASSWORD: test
TEST_DB_NAME: testdb
JWT_SECRET: test_secret
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage/lcov.info
fail_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大礼包。最后,祝您早日实现财务自由,还请给个赞,谢谢!
更多推荐
所有评论(0)