测试与质量保障
概述
软件测试是保证软件质量的核心实践。本文涵盖测试金字塔、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