自动化框架
选择和使用合适的测试自动化框架
测试框架选择指南
不同语言的主流框架对比
Java 测试框架
JUnit 5
JUnit 5 是 Java 生态系统中最流行的测试框架。
核心特性
java
// 基础注解
@DisplayName("计算器测试套件")
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class CalculatorTest {
private Calculator calculator;
@BeforeAll
static void initAll() {
System.out.println("初始化测试类");
}
@BeforeEach
void init() {
calculator = new Calculator();
}
@Test
@Order(1)
@DisplayName("加法测试")
void testAddition() {
assertEquals(4, calculator.add(2, 2));
}
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
@DisplayName("平方计算")
void testSquare(int number) {
assertEquals(number * number, calculator.square(number));
}
@TestFactory
@DisplayName("动态测试")
Stream<DynamicTest> dynamicTests() {
return Stream.of(10, 20, 30, 40)
.map(number -> DynamicTest.dynamicTest(
"测试 " + number + " 的平方",
() -> assertEquals(number * number, calculator.square(number))
));
}
@Nested
@DisplayName("除法测试")
class DivisionTests {
@Test
@DisplayName("正常除法")
void testNormalDivision() {
assertEquals(2, calculator.divide(10, 5));
}
@Test
@DisplayName("除零异常")
void testDivideByZero() {
assertThrows(ArithmeticException.class,
() -> calculator.divide(10, 0));
}
}
}高级特性
java
// 条件测试
@Test
@EnabledOnOs(OS.LINUX)
@DisplayName("仅在 Linux 上运行")
void testOnlyOnLinux() {
// Linux 特定测试
}
@Test
@EnabledIfSystemProperty(named = "env", matches = "prod")
@DisplayName("仅在生产环境运行")
void testOnlyInProduction() {
// 生产环境测试
}
// 超时测试
@Test
@Timeout(value = 2, unit = TimeUnit.SECONDS)
void testWithTimeout() {
// 必须在 2 秒内完成
}
// 重复测试
@RepeatedTest(value = 3, name = "重复测试 {currentRepetition}/{totalRepetitions}")
void repeatedTest(RepetitionInfo repetitionInfo) {
System.out.println("执行第 " + repetitionInfo.getCurrentRepetition() + " 次");
}TestNG
TestNG 提供了更强大的测试配置和并行执行能力。
xml
<!-- testng.xml -->
<!DOCTYPE suite SYSTEM "https://testng.org/testng-1.0.dtd">
<suite name="API Test Suite" parallel="methods" thread-count="5">
<test name="User Tests">
<groups>
<run>
<include name="smoke"/>
<include name="regression"/>
<exclude name="slow"/>
</run>
</groups>
<classes>
<class name="com.example.UserApiTest"/>
</classes>
</test>
</suite>java
public class UserApiTest {
@BeforeSuite
public void setupSuite() {
// 套件级别设置
}
@Test(groups = {"smoke", "user"})
public void testCreateUser() {
// 创建用户测试
}
@Test(groups = {"regression", "user"},
dependsOnMethods = "testCreateUser")
public void testUpdateUser() {
// 更新用户测试
}
@Test(dataProvider = "userData")
public void testWithData(String email, String name) {
// 数据驱动测试
}
@DataProvider(name = "userData", parallel = true)
public Object[][] userData() {
return new Object[][] {
{"user1@test.com", "User 1"},
{"user2@test.com", "User 2"},
{"user3@test.com", "User 3"}
};
}
}Mockito
Mockito 是 Java 最流行的 Mock 框架。
java
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private EmailService emailService;
@Spy
private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
@InjectMocks
private UserService userService;
@Captor
private ArgumentCaptor<User> userCaptor;
@Test
void testUserRegistration() {
// Given
RegisterRequest request = new RegisterRequest("test@example.com", "password");
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(userRepository.save(any(User.class))).thenAnswer(invocation -> {
User user = invocation.getArgument(0);
user.setId(UUID.randomUUID().toString());
return user;
});
// When
User result = userService.register(request);
// Then
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertEquals(request.getEmail(), capturedUser.getEmail());
assertTrue(passwordEncoder.matches(request.getPassword(), capturedUser.getPassword()));
verify(emailService).sendWelcomeEmail(eq(request.getEmail()), anyString());
verifyNoMoreInteractions(emailService);
}
@Test
void testMockingWithBDD() {
// Given
given(userRepository.findById("123"))
.willReturn(Optional.of(new User("123", "test@example.com")));
// When
User user = userService.getUser("123");
// Then
then(userRepository).should().findById("123");
assertThat(user.getEmail()).isEqualTo("test@example.com");
}
}JavaScript/TypeScript 测试框架
Jest
Jest 是 JavaScript 生态中功能最全面的测试框架。
typescript
// jest.config.ts
import type { Config } from 'jest';
const config: Config = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
coverageDirectory: 'coverage',
coveragePathIgnorePatterns: ['/node_modules/', '/test/'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
moduleNameMapper: {
'@/(.*)': '<rootDir>/src/$1'
},
setupFilesAfterEnv: ['<rootDir>/test/setup.ts'],
globals: {
'ts-jest': {
tsconfig: 'tsconfig.test.json'
}
}
};
export default config;typescript
// userService.test.ts
import { UserService } from '@/services/userService';
import { UserRepository } from '@/repositories/userRepository';
import { EmailService } from '@/services/emailService';
// 自动 mock
jest.mock('@/repositories/userRepository');
jest.mock('@/services/emailService');
describe('UserService', () => {
let userService: UserService;
let mockUserRepository: jest.Mocked<UserRepository>;
let mockEmailService: jest.Mocked<EmailService>;
beforeEach(() => {
// 清理 mock
jest.clearAllMocks();
// 创建 mock 实例
mockUserRepository = new UserRepository() as jest.Mocked<UserRepository>;
mockEmailService = new EmailService() as jest.Mocked<EmailService>;
userService = new UserService(mockUserRepository, mockEmailService);
});
describe('register', () => {
it('应该成功注册用户', async () => {
// Arrange
const userData = {
email: 'test@example.com',
password: 'SecurePass123!'
};
mockUserRepository.existsByEmail.mockResolvedValue(false);
mockUserRepository.save.mockImplementation(async (user) => ({
...user,
id: '123',
createdAt: new Date()
}));
// Act
const result = await userService.register(userData);
// Assert
expect(result).toMatchObject({
id: '123',
email: userData.email
});
expect(mockUserRepository.save).toHaveBeenCalledWith(
expect.objectContaining({
email: userData.email,
password: expect.not.stringMatching(userData.password) // 密码应被加密
})
);
expect(mockEmailService.sendWelcomeEmail).toHaveBeenCalledWith(
userData.email
);
});
it.each([
['', 'Password123!', 'Email is required'],
['invalid-email', 'Password123!', 'Invalid email format'],
['test@example.com', '', 'Password is required'],
['test@example.com', 'weak', 'Password too weak']
])('当 email=%s, password=%s 时应该抛出 "%s"',
async (email, password, expectedError) => {
await expect(userService.register({ email, password }))
.rejects
.toThrow(expectedError);
}
);
});
// 快照测试
describe('getUserProfile', () => {
it('应该返回正确的用户配置文件格式', async () => {
const user = {
id: '123',
email: 'test@example.com',
name: 'Test User',
createdAt: new Date('2024-01-01')
};
mockUserRepository.findById.mockResolvedValue(user);
const profile = await userService.getUserProfile('123');
expect(profile).toMatchSnapshot();
});
});
});Vitest
Vitest 是为 Vite 项目优化的现代测试框架。
typescript
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './test/setup.ts',
coverage: {
reporter: ['text', 'json', 'html'],
exclude: ['node_modules/', 'test/']
}
}
});typescript
// component.test.ts
import { describe, it, expect, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import UserProfile from '@/components/UserProfile.vue';
import { createTestingPinia } from '@pinia/testing';
describe('UserProfile', () => {
it('应该渲染用户信息', () => {
const wrapper = mount(UserProfile, {
global: {
plugins: [createTestingPinia()],
},
props: {
user: {
id: '123',
name: 'Test User',
email: 'test@example.com'
}
}
});
expect(wrapper.find('[data-test="user-name"]').text()).toBe('Test User');
expect(wrapper.find('[data-test="user-email"]').text()).toBe('test@example.com');
});
it('应该触发更新事件', async () => {
const wrapper = mount(UserProfile, {
props: {
user: { id: '123', name: 'Test User' }
}
});
await wrapper.find('[data-test="edit-button"]').trigger('click');
await wrapper.find('[data-test="name-input"]').setValue('New Name');
await wrapper.find('[data-test="save-button"]').trigger('click');
expect(wrapper.emitted()).toHaveProperty('update');
expect(wrapper.emitted('update')[0]).toEqual([
{ id: '123', name: 'New Name' }
]);
});
});Playwright
Playwright 提供跨浏览器的端到端测试能力。
typescript
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30000,
expect: {
timeout: 5000
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'results.xml' }]
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure'
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] }
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] }
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] }
},
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] }
}
],
webServer: {
command: 'npm run dev',
port: 3000,
reuseExistingServer: !process.env.CI
}
});typescript
// e2e/user-flow.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
import { DashboardPage } from './pages/DashboardPage';
test.describe('用户流程', () => {
test('完整的用户旅程', async ({ page, context }) => {
// 页面对象
const loginPage = new LoginPage(page);
const dashboardPage = new DashboardPage(page);
// 1. 登录
await loginPage.goto();
await loginPage.login('test@example.com', 'password123');
// 验证登录成功
await expect(page).toHaveURL('/dashboard');
await expect(dashboardPage.welcomeMessage).toContainText('Welcome');
// 2. 检查本地存储
const token = await context.evaluate(() => localStorage.getItem('authToken'));
expect(token).toBeTruthy();
// 3. API 拦截
await page.route('**/api/users/*', route => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({
id: '123',
name: 'Test User',
email: 'test@example.com'
})
});
});
// 4. 截图对比
await expect(page).toHaveScreenshot('dashboard.png', {
fullPage: true,
animations: 'disabled'
});
// 5. 网络监控
const [request] = await Promise.all([
page.waitForRequest(req => req.url().includes('/api/analytics')),
page.click('[data-test="track-button"]')
]);
expect(request.postDataJSON()).toMatchObject({
event: 'button_clicked'
});
});
test('响应式测试', async ({ page }) => {
// 测试不同视口
const viewports = [
{ width: 1920, height: 1080, name: 'desktop' },
{ width: 768, height: 1024, name: 'tablet' },
{ width: 375, height: 667, name: 'mobile' }
];
for (const viewport of viewports) {
await page.setViewportSize(viewport);
await page.goto('/');
await expect(page).toHaveScreenshot(`home-${viewport.name}.png`);
}
});
});Python 测试框架
pytest
pytest 是 Python 最流行的测试框架。
python
# pytest.ini
[tool:pytest]
minversion = 6.0
addopts = -ra -q --strict-markers --cov=src --cov-report=html
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
markers =
slow: marks tests as slow (deselect with '-m "not slow"')
integration: marks tests as integration tests
unit: marks tests as unit testspython
# test_user_service.py
import pytest
from unittest.mock import Mock, patch, AsyncMock
from datetime import datetime, timedelta
from src.services.user_service import UserService
from src.exceptions import ValidationError, NotFoundError
class TestUserService:
"""用户服务测试套件"""
@pytest.fixture
def user_service(self):
"""创建用户服务实例"""
repository = Mock()
cache = Mock()
email_service = Mock()
return UserService(repository, cache, email_service)
@pytest.fixture
def sample_user(self):
"""示例用户数据"""
return {
"id": "123",
"email": "test@example.com",
"name": "Test User",
"created_at": datetime.now()
}
# 参数化测试
@pytest.mark.parametrize("email,expected", [
("valid@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, expected):
"""测试邮箱验证"""
if expected:
assert user_service.validate_email(email) is True
else:
with pytest.raises(ValidationError):
user_service.validate_email(email)
# 异步测试
@pytest.mark.asyncio
async def test_async_user_creation(self, user_service):
"""测试异步用户创建"""
user_service.repository.create = AsyncMock(return_value={"id": "456"})
user_service.email_service.send_async = AsyncMock()
result = await user_service.create_user_async({
"email": "async@example.com",
"password": "password123"
})
assert result["id"] == "456"
user_service.email_service.send_async.assert_called_once()
# Fixture 参数化
@pytest.fixture(params=[
{"role": "admin", "expected_permissions": ["read", "write", "delete"]},
{"role": "user", "expected_permissions": ["read"]},
{"role": "guest", "expected_permissions": []}
])
def user_role_data(self, request):
return request.param
def test_user_permissions(self, user_service, user_role_data):
"""测试用户权限"""
role = user_role_data["role"]
expected = user_role_data["expected_permissions"]
permissions = user_service.get_permissions(role)
assert permissions == expected
# Mock 和 patch
@patch('src.services.user_service.datetime')
def test_token_generation(self, mock_datetime, user_service, sample_user):
"""测试令牌生成"""
mock_now = datetime(2024, 1, 1, 12, 0)
mock_datetime.now.return_value = mock_now
token = user_service.generate_token(sample_user["id"])
assert token is not None
assert len(token) == 32
# 验证缓存调用
user_service.cache.set.assert_called_with(
f"token:{token}",
sample_user["id"],
ttl=3600
)
# 标记测试
@pytest.mark.slow
@pytest.mark.integration
def test_heavy_operation(self, user_service):
"""测试耗时操作"""
# 这个测试会被标记为慢速测试
result = user_service.process_large_dataset()
assert result is not None
# 自定义断言
def test_custom_assertions(self, user_service, sample_user):
"""测试自定义断言"""
user_service.repository.find_by_id.return_value = sample_user
user = user_service.get_user("123")
# 使用 pytest 的丰富断言
assert user == sample_user
assert user["email"].endswith("@example.com")
assert "name" in user
assert len(user["id"]) > 0Robot Framework
Robot Framework 提供关键字驱动的测试方法。
robot
*** Settings ***
Library RequestsLibrary
Library DatabaseLibrary
Library Collections
Resource keywords/user_keywords.robot
Test Setup Setup Test Environment
Test Teardown Cleanup Test Data
*** Variables ***
${BASE_URL} http://localhost:8000
${DB_MODULE} pymysql
${DB_NAME} testdb
${DB_USER} test
${DB_PASS} test123
*** Test Cases ***
User Registration Flow
[Documentation] Test complete user registration flow
[Tags] smoke user registration
# Create new user
${user_data}= Create Dictionary
... email=test@example.com
... password=SecurePass123!
... name=Test User
${response}= Register User ${user_data}
Should Be Equal As Numbers ${response.status_code} 201
${user_id}= Get From Dictionary ${response.json()} id
# Verify in database
Connect To Database ${DB_MODULE} ${DB_NAME} ${DB_USER} ${DB_PASS}
@{result}= Query SELECT * FROM users WHERE id='${user_id}'
Should Not Be Empty ${result}
# Test login
${login_response}= Login User ${user_data}[email] ${user_data}[password]
Should Be Equal As Numbers ${login_response.status_code} 200
Dictionary Should Contain Key ${login_response.json()} token
API Performance Test
[Documentation] Test API response times
[Tags] performance
# Measure response time
${start_time}= Get Time epoch
${response}= Get User List
${end_time}= Get Time epoch
${duration}= Evaluate ${end_time} - ${start_time}
# Assert performance
Should Be True ${duration} < 1 Response time should be less than 1 second
Should Be Equal As Numbers ${response.status_code} 200
*** Keywords ***
Setup Test Environment
Create Session api ${BASE_URL}
Connect To Database ${DB_MODULE} ${DB_NAME} ${DB_USER} ${DB_PASS}
Cleanup Test Data
Execute SQL String DELETE FROM users WHERE email LIKE 'test%'
Disconnect From Database
Delete All Sessions
Register User
[Arguments] ${user_data}
${response}= POST On Session api /api/users json=${user_data}
[Return] ${response}
Login User
[Arguments] ${email} ${password}
${credentials}= Create Dictionary email=${email} password=${password}
${response}= POST On Session api /api/auth/login json=${credentials}
[Return] ${response}测试框架集成
持续集成配置
yaml
# .github/workflows/test.yml
name: Test Suite
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test-java:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Run tests
run: |
./mvnw test
./mvnw verify -Pintegration-tests
- name: Generate report
run: ./mvnw surefire-report:report
- name: Upload coverage
uses: codecov/codecov-action@v3
test-javascript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- run: npm ci
- run: npm run test:unit
- run: npm run test:integration
- run: npm run test:e2e
- name: Upload test results
uses: actions/upload-artifact@v3
with:
name: test-results
path: |
coverage/
test-results/
test-python:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install poetry
poetry install
- name: Run tests
run: |
poetry run pytest --cov=src --cov-report=xml
poetry run pytest --junit-xml=junit.xml
- name: Upload results
uses: actions/upload-artifact@v3
with:
name: python-test-results
path: |
coverage.xml
junit.xml框架选择建议
决策矩阵
| 考虑因素 | 权重 | JUnit | Jest | pytest |
|---|---|---|---|---|
| 学习曲线 | 20% | 8 | 9 | 9 |
| 功能完整性 | 25% | 9 | 9 | 10 |
| 社区支持 | 20% | 10 | 10 | 9 |
| 集成能力 | 20% | 9 | 8 | 8 |
| 性能 | 15% | 9 | 8 | 8 |
推荐场景
Java 项目
- 标准项目:JUnit 5
- 需要复杂配置:TestNG
- BDD 风格:Cucumber-JVM
JavaScript/TypeScript 项目
- React 项目:Jest + React Testing Library
- Vue 项目:Vitest + Vue Test Utils
- E2E 测试:Playwright 或 Cypress
Python 项目
- 通用项目:pytest
- Django 项目:pytest-django
- BDD 测试:behave 或 pytest-bdd
总结
选择合适的测试框架需要考虑:
- 🎯 项目需求: 匹配项目的技术栈和需求
- 👥 团队经验: 考虑团队的熟悉程度
- 🔧 工具生态: 评估周边工具支持
- 📈 可扩展性: 满足未来增长需求
- 🚀 执行效率: 保证测试运行速度
通过合理选择和使用测试框架,构建高效的自动化测试体系。