Skip to content

测试自动化

构建高效的自动化测试体系

自动化测试金字塔

自动化测试框架选择

不同技术栈的推荐框架

语言单元测试API 测试UI 测试性能测试
JavaJUnit 5, TestNGREST AssuredSelenium, PlaywrightJMeter, Gatling
Pythonpytest, unittestrequests + pytestSelenium, PlaywrightLocust
JavaScriptJest, MochaSupertest, AxiosCypress, PlaywrightK6
TypeScriptJest, VitestSupertestPlaywright, CypressK6

单元测试自动化

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() {
        // 测试实现
    }
}

自动化测试最佳实践

  1. 测试独立性: 每个测试应该独立运行
  2. 数据隔离: 使用独立的测试数据
  3. 并行执行: 提高测试执行效率
  4. 失败重试: 处理偶发性失败
  5. 清晰报告: 易于理解的测试结果

总结

构建高效的自动化测试体系需要:

  • 🎯 合适的工具: 选择适合团队的测试框架
  • 🔄 持续集成: 将测试集成到 CI/CD 流程
  • 📊 度量分析: 跟踪测试覆盖率和质量指标
  • 🚀 持续优化: 不断改进测试效率和可靠性

通过自动化测试,确保软件质量的同时提高开发效率。

SOLO Development Guide