跳到主要内容

使用 Cursor 测试代码:单元测试、集成测试和 E2E

Cursor Testing

测试是可靠软件开发的基石。Cursor 的 AI 能力可以帮助您编写全面的测试套件、识别边缘情况并维护测试覆盖。本指南涵盖单元测试、集成测试和端到端测试工作流。

前置条件​

在开始之前:

  • 安装 Cursor(建议使用最新版本)
  • 一个设置了测试框架的代码库
  • 熟悉测试概念(断言、模拟、装置)

在 Cursor 中设置测试​

项目结构​

project/
src/
components/
utils/
api/
tests/
unit/ # 单元测试
integration/ # 集成测试
e2e/ # 端到端测试
jest.config.js
playwright.config.ts
vitest.config.ts

配置 Jest 配合 TypeScript​

// jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'\\.(css|less|scss)$': 'identity-obj-proxy',
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts',
'!src/**/index.ts',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};

使用 AI 编写单元测试​

从代码生成测试​

选择一段代码并使用 Cursor 的 Ctrl+L 命令:

为此函数编写全面的单元测试:
- 正常情况
- 边缘情况(空输入、极大值、特殊字符)
- 错误处理
- 边界条件

示例:测试工具函数​

// src/utils/validation.ts
export const isValidEmail = (email: string): boolean => {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
};

export const formatDate = (
date: Date,
format: string = 'YYYY-MM-DD'
): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');

return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day);
};

Cursor 生成测试:

// tests/unit/validation.test.ts
import { isValidEmail, formatDate } from '@/utils/validation';

describe('isValidEmail', () => {
it('returns true for valid email', () => {
expect(isValidEmail('[email protected]')).toBe(true);
expect(isValidEmail('[email protected]')).toBe(true);
});

it('returns false for invalid email', () => {
expect(isValidEmail('')).toBe(false);
expect(isValidEmail('not-an-email')).toBe(false);
expect(isValidEmail('@domain.com')).toBe(false);
expect(isValidEmail('user@')).toBe(false);
expect(isValidEmail('user@domain')).toBe(false);
});

it('handles edge cases', () => {
expect(isValidEmail('[email protected]')).toBe(true);
expect(isValidEmail('[email protected]')).toBe(true);
});
});

describe('formatDate', () => {
it('formats date with default format', () => {
const date = new Date('2024-03-15');
expect(formatDate(date)).toBe('2024-03-15');
});

it('formats date with custom format', () => {
const date = new Date('2024-03-15');
expect(formatDate(date, 'DD/MM/YYYY')).toBe('15/03/2024');
expect(formatDate(date, 'MM-DD-YYYY')).toBe('03-15-2024');
});

it('handles edge cases', () => {
const janFirst = new Date('2024-01-01');
expect(formatDate(janFirst)).toBe('2024-01-01');

const dec31 = new Date('2024-12-31');
expect(formatDate(dec31)).toBe('2024-12-31');
});
});

测试 React 组件​

// tests/unit/Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { Button } from '@/components/Button';

describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});

it('calls onClick when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);

fireEvent.click(screen.getByText('Click me'));
expect(handleClick).toHaveBeenCalledTimes(1);
});

it('is disabled when loading', () => {
render(<Button isLoading>Loading</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});

it('shows loading spinner when isLoading is true', () => {
render(<Button isLoading>Loading</Button>);
expect(screen.getByTestId('spinner')).toBeInTheDocument();
});
});

集成测试​

测试 API 端点​

// tests/integration/api.test.ts
import request from 'supertest';
import { app } from '@/app';
import { prisma } from '@/db';

describe('API Integration Tests', () => {
beforeEach(async () => {
await prisma.user.deleteMany();
});

afterAll(async () => {
await prisma.$disconnect();
});

describe('POST /api/users', () => {
it('creates a new user', async () => {
const response = await request(app)
.post('/api/users')
.send({
name: 'Test User',
password: 'password123',
})
.expect(201);

expect(response.body.data.email).toBe('[email protected]');
});

it('returns 400 for invalid data', async () => {
await request(app)
.post('/api/users')
.send({
email: 'invalid',
name: '',
})
.expect(400);
});
});
});

测试数据库交互​

// tests/integration/database.test.ts
import { UserRepository } from '@/repositories/UserRepository';
import { prisma } from '@/db';

describe('UserRepository', () => {
let repository: UserRepository;

beforeEach(() => {
repository = new UserRepository();
});

afterEach(async () => {
await prisma.user.deleteMany();
});

it('creates and retrieves a user', async () => {
const user = await repository.create({
name: 'Test User',
});

expect(user.id).toBeDefined();

const found = await repository.findById(user.id);
expect(found?.email).toBe('[email protected]');
});
});

端到端测试配合 Playwright​

设置 Playwright​

npm init playwright@latest

生成 E2E 测试​

// tests/e2e/login.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Authentication Flow', () => {
test('user can log in', async ({ page }) => {
await page.goto('/login');

await page.fill('[data-testid="email-input"]', '[email protected]');
await page.fill('[data-testid="password-input"]', 'password123');
await page.click('[data-testid="login-button"]');

await expect(page).toHaveURL('/dashboard');
await expect(page.locator('[data-testid="welcome-message"]')).toContainText(
'Welcome'
);
});

test('shows error for invalid credentials', async ({ page }) => {
await page.goto('/login');

await page.fill('[data-testid="email-input"]', '[email protected]');
await page.fill('[data-testid="password-input"]', 'wrongpassword');
await page.click('[data-testid="login-button"]');

await expect(page.locator('[data-testid="error-message"]')).toContainText(
'Invalid credentials'
);
});
});

使用 Cursor 的页面对象模式​

// tests/e2e/pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';

export class LoginPage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;

constructor(private page: Page) {
this.emailInput = page.locator('[data-testid="email-input"]');
this.passwordInput = page.locator('[data-testid="password-input"]');
this.loginButton = page.locator('[data-testid="login-button"]');
this.errorMessage = page.locator('[data-testid="error-message"]');
}

async goto() {
await this.page.goto('/login');
}

async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}

使用 Cursor 进行测试驱动开发​

红绿重构循环​

  1. 红:编写测试描述您期望的行为
  2. 绿:使用 Cursor 生成实现代码
  3. 重构:要求 Cursor 在保持测试通过的同时清理代码

示例:TDD 配合 Cursor​

// 步骤 1:编写测试
// tests/unit/calculator.test.ts
import { Calculator } from '@/utils/Calculator';

describe('Calculator', () => {
it('adds two numbers', () => {
const calc = new Calculator();
expect(calc.add(2, 3)).toBe(5);
});

it('handles negative numbers', () => {
const calc = new Calculator();
expect(calc.add(-2, 3)).toBe(1);
});
});
// 步骤 2:Cursor 生成实现
// src/utils/Calculator.ts
export class Calculator {
add(a: number, b: number): number {
return a + b;
}
}

模拟和存根​

Jest 模拟​

// tests/unit/api.test.ts
import { fetchUserData } from '@/api/users';
import { getDatabaseConnection } from '@/db';

jest.mock('@/db', () => ({
getDatabaseConnection: jest.fn(),
}));

describe('fetchUserData', () => {
it('returns user data from database', async () => {
const mockQuery = jest.fn().mockResolvedValue({
rows: [{ id: 1, name: 'Test User' }],
});

(getDatabaseConnection as jest.Mock).mockReturnValue({
query: mockQuery,
});

const result = await fetchUserData(1);
expect(result).toEqual({ id: 1, name: 'Test User' });
expect(mockQuery).toHaveBeenCalledWith(
'SELECT * FROM users WHERE id = $1',
[1]
);
});
});

MSW 用于 API 模拟​

// tests/mocks/handlers.ts
import { rest } from 'msw';

export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.json({
users: [
{ id: 1, name: 'Test User' },
{ id: 2, name: 'Another User' },
],
})
);
}),

rest.post('/api/users', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.status(201),
ctx.json({ id: 3, ...body })
);
}),
];

测试覆盖率和质量​

生成覆盖报告​

# Jest
npx jest --coverage

# Vitest
npx vitest --coverage

# 在 Cursor 中查看
# 点击终端中的 HTML 报告链接

Cursor 的覆盖分析​

分析此测试套件:
1. 测试覆盖了哪些边缘情况?
2. 存在哪些测试空白?
3. 如何提高分支覆盖?

持续集成设置​

GitHub Actions​

# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm run test:unit
- run: npm run test:integration
- run: npm run test:e2e

测试快速参考​

框架用途Cursor 提示
Jest单元/集成"使用 Jest 为此组件编写测试"
Vitest快速单元测试"使用 Vitest 生成测试,覆盖边缘情况"
PlaywrightE2E 浏览器"为登录流程创建 Playwright 测试"
CypressE2E 浏览器"编写 Cypress 测试用于表单提交"
MSWAPI 模拟"设置 MSW 处理程序用于用户 API"
React Testing Library组件测试"使用 RTL 测试此 React 组件"

常见测试模式​

模式示例
工厂函数createUser({ role: 'admin' })
设置/清理beforeEach / afterEach
参数化测试it.each([[1, 2, 3], [4, 5, 9]])
快照测试expect(tree).toMatchSnapshot()
间谍jest.spyOn(console, 'error')

相关资源​

最后更新:2025 年 7 月