跳转至

测试与质量保障

概述

软件测试是保证软件质量的核心实践。本文涵盖测试金字塔、TDD、测试工具、代码审查、静态分析和性能/安全测试。


1. 测试金字塔

        /\
       /  \        E2E 测试(少量)
      /────\       UI 自动化、端到端流程
     /      \
    /────────\     集成测试(适量)
   /          \    API 测试、组件交互
  /────────────\   单元测试(大量)
 /              \  函数/类/方法级别
/________________\
层次 数量 速度 维护成本 置信度
单元测试 多(70%) 极快 低(单个组件)
集成测试 中(20%) 较快 中(组件交互)
E2E 测试 少(10%) 高(全流程)

2. TDD(测试驱动开发)

Red-Green-Refactor 循环

1. Red:     写一个失败的测试
2. Green:   写最少的代码让测试通过
3. Refactor: 重构代码,保持测试通过
↻ 重复

示例

# Step 1: Red - 写测试
def test_calculate_discount():
    assert calculate_discount(100, 0.1) == 90.0
    assert calculate_discount(200, 0.25) == 150.0
    assert calculate_discount(100, 0) == 100.0

# Step 2: Green - 最简实现
def calculate_discount(price, discount_rate):
    return price * (1 - discount_rate)

# Step 3: Refactor - 增加边界检查
def calculate_discount(price, discount_rate):
    if price < 0:
        raise ValueError("Price must be non-negative")
    if not 0 <= discount_rate <= 1:
        raise ValueError("Discount rate must be between 0 and 1")
    return round(price * (1 - discount_rate), 2)

3. 单元测试

3.1 pytest(Python)

import pytest
from myapp.user_service import UserService, UserNotFoundError

class TestUserService:
    @pytest.fixture
    def service(self):
        """每个测试方法前创建新的 service 实例"""
        return UserService(db=MockDatabase())

    def test_get_user_by_id(self, service):
        user = service.get_user(1)
        assert user.name == "Alice"
        assert user.email == "alice@example.com"

    def test_get_nonexistent_user_raises(self, service):
        with pytest.raises(UserNotFoundError):
            service.get_user(999)

    @pytest.mark.parametrize("email,valid", [
        ("user@example.com", True),
        ("user@", False),
        ("", False),
        ("user@domain.co.uk", True),
    ])
    def test_email_validation(self, service, email, valid):
        assert service.validate_email(email) == valid

    def test_create_user(self, service):
        user = service.create_user("Bob", "bob@example.com")
        assert user.id is not None
        assert user.name == "Bob"

3.2 JUnit(Java)

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void shouldReturnUserById() {
        when(userRepository.findById(1L))
            .thenReturn(Optional.of(new User(1L, "Alice")));

        User user = userService.getUser(1L);

        assertEquals("Alice", user.getName());
        verify(userRepository).findById(1L);
    }

    @Test
    void shouldThrowWhenUserNotFound() {
        when(userRepository.findById(999L))
            .thenReturn(Optional.empty());

        assertThrows(UserNotFoundException.class, 
            () -> userService.getUser(999L));
    }
}

4. Mocking

Mock 对象替代真实依赖,隔离被测单元。

from unittest.mock import Mock, patch, MagicMock

class TestOrderService:
    def test_place_order_sends_email(self):
        # 创建 Mock 对象
        mock_email_service = Mock()
        mock_payment_service = Mock()
        mock_payment_service.charge.return_value = True

        service = OrderService(
            email=mock_email_service,
            payment=mock_payment_service
        )

        service.place_order(user_id=1, amount=99.99)

        # 验证交互
        mock_payment_service.charge.assert_called_once_with(1, 99.99)
        mock_email_service.send_confirmation.assert_called_once()

    @patch('myapp.services.requests.get')
    def test_fetch_external_data(self, mock_get):
        """用 patch 替换外部 HTTP 调用"""
        mock_get.return_value.json.return_value = {"data": "test"}
        mock_get.return_value.status_code = 200

        result = fetch_data("https://api.example.com")
        assert result == {"data": "test"}

何时使用 Mock

场景 是否 Mock
数据库调用 是(单元测试中)
HTTP 请求
文件系统 视情况
纯函数
内部方法 通常不

5. 代码覆盖率

# pytest + coverage
pytest --cov=src --cov-report=html tests/

# 输出
# Name                 Stmts   Miss  Cover
# ----------------------------------------
# src/models.py           45      3    93%
# src/services.py         78     12    85%
# src/utils.py            23      0   100%
# ----------------------------------------
# TOTAL                  146     15    90%

覆盖率类型:

类型 含义
行覆盖 哪些行被执行了
分支覆盖 if/else 的每个分支是否都被测试
条件覆盖 复合条件的每个子条件
路径覆盖 所有可能的执行路径

覆盖率不等于质量

100% 覆盖率并不意味着没有 bug。测试质量取决于断言的有效性,而非覆盖率数字。


6. 代码审查(Code Review)

6.1 审查清单

维度 关注点
正确性 逻辑是否正确?边界条件?错误处理?
安全性 输入验证?SQL 注入?XSS?敏感数据?
性能 N+1 查询?不必要的计算?内存泄漏?
可读性 命名清晰?注释适当?复杂度合理?
可维护性 耦合度?单一职责?是否需要拆分?
测试 有测试吗?测试覆盖关键路径?

6.2 最佳实践

  • PR 大小控制在 200-400 行以内
  • 每次审查花费不超过 60 分钟
  • 提供建设性反馈,区分必须修改 vs 建议
  • 自动化能做的交给 CI(格式、lint、测试)
  • 关注设计和逻辑,而非风格(风格交给工具)

7. 静态分析

7.1 工具

工具 语言 功能
pylint / ruff Python 代码质量、风格检查
mypy / pyright Python 静态类型检查
ESLint JavaScript/TS 代码质量
SonarQube 多语言 综合质量平台
Bandit Python 安全漏洞检查
SpotBugs Java Bug 检测

7.2 示例:ruff(Python)

# pyproject.toml
[tool.ruff]
line-length = 100
target-version = "py311"

[tool.ruff.lint]
select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # pyflakes
    "I",    # isort
    "N",    # pep8-naming
    "UP",   # pyupgrade
    "S",    # bandit (security)
    "B",    # bugbear
]
ruff check src/             # 检查
ruff check --fix src/       # 自动修复
ruff format src/            # 格式化

8. 性能测试

8.1 类型

类型 目的 方法
负载测试 验证预期负载下的性能 模拟正常用户数
压力测试 找到系统极限 持续增加负载直到崩溃
浸泡测试 检查长时间运行的问题 长时间中等负载
峰值测试 验证突发流量处理 突然大幅增加负载

8.2 工具

# Locust 示例
from locust import HttpUser, task, between

class WebsiteUser(HttpUser):
    wait_time = between(1, 3)

    @task(3)
    def view_items(self):
        self.client.get("/api/items")

    @task(1)
    def create_order(self):
        self.client.post("/api/orders", json={
            "item_id": 1,
            "quantity": 2
        })
# 运行 Locust
locust -f load_test.py --host=http://localhost:8000
# 或命令行模式
locust -f load_test.py --headless -u 1000 -r 50 --run-time 5m

关键指标:

  • 响应时间:P50、P95、P99
  • 吞吐量:RPS(Requests Per Second)
  • 错误率:失败请求占比
  • 资源使用:CPU、内存、网络、磁盘 I/O

9. 安全测试

9.1 OWASP Top 10

排名 威胁 防护
1 访问控制失效 最小权限、默认拒绝
2 加密失败 TLS、安全哈希、密钥管理
3 注入 参数化查询、输入验证
4 不安全设计 威胁建模、安全设计原则
5 安全配置错误 最小安装、强化配置
6 过时组件 依赖扫描(Dependabot)
7 认证失败 MFA、安全密码策略
8 数据完整性失败 签名验证、CI/CD 安全
9 日志监控不足 集中日志、告警
10 SSRF URL 白名单、网络隔离

9.2 安全测试工具

类型 工具 说明
SAST(静态) Bandit, Semgrep 分析源代码
DAST(动态) OWASP ZAP, Burp Suite 运行时扫描
SCA(依赖) Dependabot, Snyk 依赖漏洞
容器 Trivy, Clair 镜像安全扫描
密钥 TruffleHog, GitLeaks 检测泄漏的密钥

与其他主题的关系

  • 参见 版本控制与CI/CD,理解测试如何嵌入持续集成和持续发布流程
  • 参见 系统设计,理解质量属性如何在架构阶段就被设计进去
  • 参见 软件工程概述,理解验证链条在软件生命周期中的位置
  • 参见 全栈开发,理解单元测试、集成测试与端到端测试如何跨层协同

参考文献

  • "Unit Testing Principles, Practices, and Patterns" - Vladimir Khorikov
  • "Clean Code" - Robert C. Martin
  • OWASP Testing Guide: https://owasp.org/www-project-testing-guide/
  • pytest 官方文档:https://docs.pytest.org

评论 #