测试自动化
构建高效的自动化测试体系
自动化测试金字塔
自动化测试框架选择
不同技术栈的推荐框架
| 语言 | 单元测试 | API 测试 | UI 测试 | 性能测试 |
|---|---|---|---|---|
| Java | JUnit 5, TestNG | REST Assured | Selenium, Playwright | JMeter, Gatling |
| Python | pytest, unittest | requests + pytest | Selenium, Playwright | Locust |
| JavaScript | Jest, Mocha | Supertest, Axios | Cypress, Playwright | K6 |
| TypeScript | Jest, Vitest | Supertest | Playwright, Cypress | K6 |
单元测试自动化
Java - JUnit 5
java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Nested
@DisplayName("用户注册测试")
class UserRegistrationTests {
@Test
@DisplayName("成功注册新用户")
void shouldRegisterUserSuccessfully() {
// Given
RegisterRequest request = RegisterRequest.builder()
.email("test@example.com")
.password("SecurePass123!")
.username("testuser")
.build();
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(userRepository.save(any(User.class))).thenAnswer(i -> {
User user = i.getArgument(0);
user.setId(UUID.randomUUID().toString());
return user;
});
// When
User result = userService.register(request);
// Then
assertThat(result).isNotNull();
assertThat(result.getId()).isNotNull();
assertThat(result.getEmail()).isEqualTo("test@example.com");
verify(emailService).sendWelcomeEmail(eq("test@example.com"), anyString());
}
@ParameterizedTest
@CsvSource({
"test@example.com, true",
"invalid-email, false",
"@example.com, false",
"test@, false"
})
@DisplayName("邮箱格式验证")
void shouldValidateEmailFormat(String email, boolean isValid) {
// Given
RegisterRequest request = RegisterRequest.builder()
.email(email)
.password("Password123!")
.username("user")
.build();
// When & Then
if (isValid) {
assertDoesNotThrow(() -> userService.validateRequest(request));
} else {
assertThrows(ValidationException.class,
() -> userService.validateRequest(request));
}
}
}
}Python - pytest
python
# test_user_service.py
import pytest
from unittest.mock import Mock, patch
from datetime import datetime
from app.services.user_service import UserService
from app.models import User
from app.exceptions import ValidationError, DuplicateEmailError
class TestUserService:
"""用户服务测试"""
@pytest.fixture
def user_service(self):
"""创建用户服务实例"""
repository = Mock()
email_service = Mock()
return UserService(repository, email_service)
@pytest.fixture
def valid_user_data(self):
"""有效的用户数据"""
return {
'email': 'test@example.com',
'password': 'SecurePass123!',
'username': 'testuser'
}
def test_register_user_successfully(self, user_service, valid_user_data):
"""测试成功注册用户"""
# Arrange
user_service.repository.exists_by_email.return_value = False
user_service.repository.save.return_value = User(
id='123',
**valid_user_data,
created_at=datetime.now()
)
# Act
result = user_service.register(valid_user_data)
# Assert
assert result.id == '123'
assert result.email == valid_user_data['email']
user_service.email_service.send_welcome_email.assert_called_once()
@pytest.mark.parametrize("email,is_valid", [
("test@example.com", True),
("user.name@domain.co.uk", True),
("invalid-email", False),
("@example.com", False),
("test@", False),
])
def test_email_validation(self, user_service, email, is_valid):
"""测试邮箱格式验证"""
if is_valid:
assert user_service.validate_email(email) is True
else:
with pytest.raises(ValidationError):
user_service.validate_email(email)
def test_duplicate_email_raises_error(self, user_service, valid_user_data):
"""测试重复邮箱抛出错误"""
# Arrange
user_service.repository.exists_by_email.return_value = True
# Act & Assert
with pytest.raises(DuplicateEmailError) as exc_info:
user_service.register(valid_user_data)
assert str(exc_info.value) == f"Email {valid_user_data['email']} already exists"TypeScript - Jest
typescript
// userService.test.ts
import { UserService } from '../src/services/userService';
import { UserRepository } from '../src/repositories/userRepository';
import { EmailService } from '../src/services/emailService';
import { RegisterRequest, User } from '../src/types';
// Mock 依赖
jest.mock('../src/repositories/userRepository');
jest.mock('../src/services/emailService');
describe('UserService', () => {
let userService: UserService;
let userRepository: jest.Mocked<UserRepository>;
let emailService: jest.Mocked<EmailService>;
beforeEach(() => {
userRepository = new UserRepository() as jest.Mocked<UserRepository>;
emailService = new EmailService() as jest.Mocked<EmailService>;
userService = new UserService(userRepository, emailService);
});
describe('register', () => {
const validRequest: RegisterRequest = {
email: 'test@example.com',
password: 'SecurePass123!',
username: 'testuser'
};
it('应该成功注册新用户', async () => {
// Arrange
userRepository.existsByEmail.mockResolvedValue(false);
userRepository.save.mockImplementation(async (user) => ({
...user,
id: '123',
createdAt: new Date()
}));
// Act
const result = await userService.register(validRequest);
// Assert
expect(result).toMatchObject({
id: '123',
email: validRequest.email,
username: validRequest.username
});
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith(
validRequest.email,
validRequest.username
);
});
it('邮箱已存在时应该抛出错误', async () => {
// Arrange
userRepository.existsByEmail.mockResolvedValue(true);
// Act & Assert
await expect(userService.register(validRequest))
.rejects
.toThrow('Email already exists');
});
describe('参数验证', () => {
test.each([
['', 'Password123!', 'user', 'Email is required'],
['test@example.com', '', 'user', 'Password is required'],
['test@example.com', 'short', 'user', 'Password must be at least 8 characters'],
['invalid-email', 'Password123!', 'user', 'Invalid email format']
])(
'当 email=%s, password=%s, username=%s 时应该抛出 "%s"',
async (email, password, username, expectedError) => {
const request = { email, password, username };
await expect(userService.register(request))
.rejects
.toThrow(expectedError);
}
);
});
});
});API 测试自动化
REST Assured (Java)
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@TestPropertySource(locations = "classpath:application-test.properties")
public class UserApiTest {
@LocalServerPort
private int port;
@Autowired
private TestRestTemplate restTemplate;
@BeforeEach
public void setup() {
RestAssured.port = port;
RestAssured.basePath = "/api";
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
@Test
@DisplayName("创建用户 - 成功场景")
public void createUser_Success() {
Map<String, Object> request = Map.of(
"email", "newuser@example.com",
"password", "SecurePass123!",
"username", "newuser"
);
given()
.contentType(ContentType.JSON)
.body(request)
.when()
.post("/users")
.then()
.statusCode(201)
.header("Location", matchesPattern(".*/users/[a-f0-9-]+$"))
.body("id", notNullValue())
.body("email", equalTo("newuser@example.com"))
.body("username", equalTo("newuser"))
.body("createdAt", notNullValue())
.time(lessThan(2000L)); // 响应时间小于2秒
}
@Test
@DisplayName("获取用户列表 - 分页")
public void getUsers_Pagination() {
given()
.queryParam("page", 0)
.queryParam("size", 10)
.queryParam("sort", "createdAt,desc")
.when()
.get("/users")
.then()
.statusCode(200)
.body("content", hasSize(lessThanOrEqualTo(10)))
.body("pageable.pageNumber", equalTo(0))
.body("pageable.pageSize", equalTo(10))
.body("totalElements", greaterThanOrEqualTo(0))
.body("content[0].id", notNullValue());
}
}Supertest (JavaScript/TypeScript)
typescript
// user.api.test.ts
import request from 'supertest';
import { app } from '../src/app';
import { database } from '../src/database';
describe('User API', () => {
beforeAll(async () => {
await database.connect();
});
afterAll(async () => {
await database.disconnect();
});
beforeEach(async () => {
await database.clear();
});
describe('POST /api/users', () => {
it('应该创建新用户', async () => {
const userData = {
email: 'test@example.com',
password: 'SecurePass123!',
username: 'testuser'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect('Content-Type', /json/)
.expect(201);
expect(response.body).toMatchObject({
id: expect.any(String),
email: userData.email,
username: userData.username
});
expect(response.headers.location).toMatch(/\/api\/users\/[\w-]+$/);
});
it('应该验证必填字段', async () => {
const response = await request(app)
.post('/api/users')
.send({})
.expect(400);
expect(response.body).toMatchObject({
errors: expect.arrayContaining([
expect.objectContaining({
field: 'email',
message: 'Email is required'
}),
expect.objectContaining({
field: 'password',
message: 'Password is required'
})
])
});
});
});
describe('GET /api/users/:id', () => {
it('应该返回用户详情', async () => {
// 创建测试用户
const user = await createTestUser();
const response = await request(app)
.get(`/api/users/${user.id}`)
.expect(200);
expect(response.body).toMatchObject({
id: user.id,
email: user.email,
username: user.username
});
});
it('用户不存在时返回404', async () => {
const response = await request(app)
.get('/api/users/non-existent-id')
.expect(404);
expect(response.body).toMatchObject({
error: 'User not found'
});
});
});
});UI 测试自动化
Playwright
typescript
// e2e/user-registration.spec.ts
import { test, expect } from '@playwright/test';
import { UserPage } from './pages/UserPage';
test.describe('用户注册流程', () => {
let userPage: UserPage;
test.beforeEach(async ({ page }) => {
userPage = new UserPage(page);
await userPage.goto();
});
test('成功注册新用户', async ({ page }) => {
// 填写注册表单
await userPage.fillRegistrationForm({
email: `test${Date.now()}@example.com`,
password: 'SecurePass123!',
username: `user${Date.now()}`
});
// 提交表单
await userPage.submitRegistration();
// 验证成功消息
await expect(page.locator('.success-message'))
.toContainText('注册成功');
// 验证跳转到仪表板
await expect(page).toHaveURL('/dashboard');
});
test('显示验证错误', async ({ page }) => {
// 提交空表单
await userPage.submitRegistration();
// 验证错误消息
await expect(page.locator('.error-email'))
.toContainText('邮箱不能为空');
await expect(page.locator('.error-password'))
.toContainText('密码不能为空');
});
test('实时密码强度指示', async ({ page }) => {
const passwordInput = page.locator('#password');
const strengthIndicator = page.locator('.password-strength');
// 弱密码
await passwordInput.fill('123456');
await expect(strengthIndicator).toHaveClass(/weak/);
// 中等密码
await passwordInput.fill('Test123');
await expect(strengthIndicator).toHaveClass(/medium/);
// 强密码
await passwordInput.fill('SecurePass123!@#');
await expect(strengthIndicator).toHaveClass(/strong/);
});
});
// pages/UserPage.ts
export class UserPage {
constructor(private page: Page) {}
async goto() {
await this.page.goto('/register');
}
async fillRegistrationForm(data: {
email: string;
password: string;
username: string;
}) {
await this.page.fill('#email', data.email);
await this.page.fill('#password', data.password);
await this.page.fill('#username', data.username);
}
async submitRegistration() {
await this.page.click('button[type="submit"]');
}
}Cypress
javascript
// cypress/e2e/user-journey.cy.js
describe('用户完整流程', () => {
beforeEach(() => {
cy.task('db:seed');
cy.visit('/');
});
it('从注册到首次使用的完整流程', () => {
// 1. 注册
cy.get('[data-cy=register-link]').click();
const userData = {
email: `test${Date.now()}@example.com`,
password: 'SecurePass123!',
username: `user${Date.now()}`
};
cy.get('#email').type(userData.email);
cy.get('#password').type(userData.password);
cy.get('#username').type(userData.username);
cy.get('[data-cy=register-submit]').click();
// 验证注册成功
cy.url().should('include', '/welcome');
cy.contains('欢迎加入').should('be.visible');
// 2. 邮箱验证
cy.task('getLastEmail', userData.email).then((email) => {
const verificationLink = email.body.match(/href="([^"]+verify[^"]+)"/)[1];
cy.visit(verificationLink);
});
cy.contains('邮箱验证成功').should('be.visible');
// 3. 完善个人资料
cy.get('[data-cy=complete-profile]').click();
cy.get('#firstName').type('Test');
cy.get('#lastName').type('User');
cy.get('#phone').type('+1234567890');
cy.get('[data-cy=save-profile]').click();
// 4. 首次使用引导
cy.get('[data-cy=start-tour]').click();
cy.get('.tour-step-1').should('be.visible');
cy.get('[data-cy=tour-next]').click();
cy.get('.tour-step-2').should('be.visible');
cy.get('[data-cy=tour-finish]').click();
// 验证进入主界面
cy.url().should('include', '/dashboard');
cy.contains(`欢迎,${userData.username}`).should('be.visible');
});
});性能测试自动化
K6
javascript
// performance/user-api-load.js
import http from 'k6/http';
import { check, sleep } from 'k6';
import { Rate } from 'k6/metrics';
// 自定义指标
const errorRate = new Rate('errors');
// 测试配置
export const options = {
stages: [
{ duration: '2m', target: 100 }, // 逐步增加到 100 用户
{ duration: '5m', target: 100 }, // 保持 100 用户持续 5 分钟
{ duration: '2m', target: 200 }, // 增加到 200 用户
{ duration: '5m', target: 200 }, // 保持 200 用户
{ duration: '2m', target: 0 }, // 逐步降到 0
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% 请求在 500ms 内
errors: ['rate<0.01'], // 错误率低于 1%
http_req_failed: ['rate<0.01'] // 失败率低于 1%
},
};
export default function() {
// 1. 获取用户列表
const listResponse = http.get('https://api.example.com/users');
check(listResponse, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
errorRate.add(listResponse.status !== 200);
sleep(1);
// 2. 创建新用户
const payload = JSON.stringify({
email: `user${__VU}${Date.now()}@example.com`,
password: 'TestPass123!',
username: `user${__VU}${Date.now()}`
});
const params = {
headers: {
'Content-Type': 'application/json',
},
};
const createResponse = http.post(
'https://api.example.com/users',
payload,
params
);
check(createResponse, {
'user created': (r) => r.status === 201,
'has user id': (r) => JSON.parse(r.body).id !== undefined,
});
errorRate.add(createResponse.status !== 201);
sleep(2);
}
export function handleSummary(data) {
return {
'summary.html': htmlReport(data),
stdout: textSummary(data, { indent: ' ', enableColors: true }),
};
}持续集成自动化
Jenkins Pipeline
groovy
pipeline {
agent any
tools {
maven 'Maven 3.8.1'
nodejs 'Node 16'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Backend Tests') {
parallel {
stage('Unit Tests') {
steps {
sh 'mvn clean test'
}
post {
always {
junit '**/target/surefire-reports/*.xml'
jacoco(
execPattern: '**/target/*.exec',
classPattern: '**/target/classes',
sourcePattern: '**/src/main/java'
)
}
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify -Pintegration-tests'
}
}
}
}
stage('Frontend Tests') {
steps {
dir('frontend') {
sh 'npm ci'
sh 'npm run test:unit -- --coverage'
sh 'npm run test:integration'
}
}
post {
always {
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'frontend/coverage/lcov-report',
reportFiles: 'index.html',
reportName: 'Frontend Coverage Report'
])
}
}
}
stage('E2E Tests') {
when {
branch 'main'
}
steps {
sh 'docker-compose up -d'
sh 'npm run test:e2e'
}
post {
always {
sh 'docker-compose down'
archiveArtifacts artifacts: 'e2e/screenshots/**/*.png', allowEmptyArchive: true
archiveArtifacts artifacts: 'e2e/videos/**/*.mp4', allowEmptyArchive: true
}
}
}
stage('Performance Tests') {
when {
branch 'main'
}
steps {
sh 'k6 run performance/user-api-load.js'
}
}
stage('Security Scan') {
steps {
sh 'npm audit'
sh 'mvn dependency-check:check'
}
}
}
post {
always {
emailext(
subject: "Build ${currentBuild.currentResult}: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: '${SCRIPT, template="groovy-html.template"}',
recipientProviders: [developers()]
)
}
}
}测试数据管理
测试数据工厂
typescript
// test/factories/userFactory.ts
import { Factory } from 'fishery';
import { faker } from '@faker-js/faker';
import { User } from '../src/models/User';
export const userFactory = Factory.define<User>(() => ({
id: faker.datatype.uuid(),
email: faker.internet.email(),
username: faker.internet.userName(),
password: faker.internet.password(12),
firstName: faker.name.firstName(),
lastName: faker.name.lastName(),
createdAt: faker.date.past(),
updatedAt: faker.date.recent()
}));
// 使用工厂
const testUsers = userFactory.buildList(10);
const adminUser = userFactory.build({
role: 'admin',
email: 'admin@example.com'
});测试报告和分析
Allure 报告集成
java
@Epic("用户管理")
@Feature("用户注册")
@Story("新用户注册流程")
public class UserRegistrationTest {
@Test
@Description("验证用户可以使用有效信息成功注册")
@Severity(SeverityLevel.CRITICAL)
@Link(name = "需求文档", url = "https://docs.example.com/requirements/user-registration")
@TmsLink("TMS-123")
public void testSuccessfulRegistration() {
// 测试实现
}
}自动化测试最佳实践
- 测试独立性: 每个测试应该独立运行
- 数据隔离: 使用独立的测试数据
- 并行执行: 提高测试执行效率
- 失败重试: 处理偶发性失败
- 清晰报告: 易于理解的测试结果
总结
构建高效的自动化测试体系需要:
- 🎯 合适的工具: 选择适合团队的测试框架
- 🔄 持续集成: 将测试集成到 CI/CD 流程
- 📊 度量分析: 跟踪测试覆盖率和质量指标
- 🚀 持续优化: 不断改进测试效率和可靠性
通过自动化测试,确保软件质量的同时提高开发效率。