Skip to content

自动化框架

选择和使用合适的测试自动化框架

测试框架选择指南

不同语言的主流框架对比

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 tests
python
# 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"]) > 0

Robot 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

框架选择建议

决策矩阵

考虑因素权重JUnitJestpytest
学习曲线20%899
功能完整性25%9910
社区支持20%10109
集成能力20%988
性能15%988

推荐场景

  1. Java 项目

    • 标准项目:JUnit 5
    • 需要复杂配置:TestNG
    • BDD 风格:Cucumber-JVM
  2. JavaScript/TypeScript 项目

    • React 项目:Jest + React Testing Library
    • Vue 项目:Vitest + Vue Test Utils
    • E2E 测试:Playwright 或 Cypress
  3. Python 项目

    • 通用项目:pytest
    • Django 项目:pytest-django
    • BDD 测试:behave 或 pytest-bdd

总结

选择合适的测试框架需要考虑:

  • 🎯 项目需求: 匹配项目的技术栈和需求
  • 👥 团队经验: 考虑团队的熟悉程度
  • 🔧 工具生态: 评估周边工具支持
  • 📈 可扩展性: 满足未来增长需求
  • 🚀 执行效率: 保证测试运行速度

通过合理选择和使用测试框架,构建高效的自动化测试体系。

SOLO Development Guide