1. 单元测试
概念
- 定义: 单元测试是对代码中最小功能单元的测试,通常是函数或类的方法。
- 目标: 验证单个功能是否按照预期工作,而不依赖其他模块或外部资源。
- 特点: 快速、独立,通常是开发者最先编写的测试。
示例:pytest 实现单元测试
python
# 功能模块:一个简单的数学函数
def add(x, y):
"""
加法函数
"""
return x + y
def divide(x, y):
"""
除法函数,包含除零检查
"""
if y == 0:
raise ValueError("Cannot divide by zero")
return x / y
# 测试模块:单元测试
def test_add():
"""
测试 add 函数
"""
assert add(2, 3) == 5 # 正常情况
assert add(-1, 1) == 0 # 边界值
assert add(0, 0) == 0 # 特殊情况
def test_divide():
"""
测试 divide 函数
"""
assert divide(10, 2) == 5 # 正常情况
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(1, 0) # 测试除零异常
执行命令
运行单元测试:
bash
pytest test_example.py
优点:
- 快速反馈代码问题。
- 单一功能模块的高覆盖率。
2. 集成测试
概念
- 定义: 集成测试是验证多个模块的交互行为是否正常,确保它们组合在一起能够按预期工作。
- 目标: 检查模块之间的接口和协作行为,可能涉及数据库、API 或文件系统等外部依赖。
- 特点: 比单元测试慢,但更贴近实际场景。
示例:pytest 实现集成测试
使用数据库模拟的场景
假设我们有一个用户管理模块,需要测试用户的创建、查询和删除功能:
python
# 功能模块:用户管理
class UserDatabase:
"""
模拟用户数据库
"""
def __init__(self):
self.users = {}
def add_user(self, username, email):
"""
添加用户
"""
if username in self.users:
raise ValueError("User already exists")
self.users[username] = email
def get_user(self, username):
"""
获取用户
"""
return self.users.get(username)
def delete_user(self, username):
"""
删除用户
"""
if username in self.users:
del self.users[username]
else:
raise ValueError("User does not exist")
# 测试模块:集成测试
def test_user_database():
"""
测试用户数据库模块的集成功能
"""
db = UserDatabase()
# 添加用户
db.add_user("alice", "alice@example.com")
assert db.get_user("alice") == "alice@example.com"
# 删除用户
db.delete_user("alice")
assert db.get_user("alice") is None
# 测试异常情况
with pytest.raises(ValueError, match="User does not exist"):
db.delete_user("alice")
执行命令
运行集成测试:
bash
pytest test_example.py
单元测试与集成测试的区别
特性 | 单元测试 | 集成测试 |
---|---|---|
测试范围 | 单一模块或函数 | 多个模块之间的交互 |
目标 | 验证单独功能是否正确 | 验证整体功能是否按预期工作 |
速度 | 快速 | 较慢 |
复杂度 | 较低 | 较高,可能涉及外部依赖 |
测试工具 | 模拟对象 (Mock) | 实际环境或部分模拟环境 |
3. pytest 中的 Mock 模拟(用于集成测试中的外部依赖)
在集成测试中,我们可能需要模拟外部依赖(如数据库、API)。pytest
支持使用 unittest.mock
来实现 Mock。
示例:模拟外部 API
假设我们有一个函数需要从外部 API 获取数据:
python
# 功能模块:从外部 API 获取数据
def fetch_data(api_client):
"""
从外部 API 客户端获取数据
"""
response = api_client.get("/data")
if response.status_code == 200:
return response.json()
else:
raise ValueError("Failed to fetch data")
测试:使用 Mock 模拟 API
python
from unittest.mock import MagicMock
def test_fetch_data():
"""
测试 fetch_data 函数,使用 Mock 模拟 API 行为
"""
# 创建 Mock API 客户端
mock_client = MagicMock()
# 模拟成功响应
mock_client.get.return_value.status_code = 200
mock_client.get.return_value.json.return_value = {"key": "value"}
# 调用函数并验证返回值
result = fetch_data(mock_client)
assert result == {"key": "value"}
# 验证 API 是否被正确调用
mock_client.get.assert_called_once_with("/data")
运行测试
使用以下命令运行测试:
bash
pytest test_example.py
4. 测试组合:单元测试 + 集成测试
实际开发中,建议结合单元测试和集成测试:
- 单元测试:覆盖每个功能单元,确保模块内部逻辑正确。
- 集成测试:验证模块之间的交互和整体功能。
最佳实践
- 单元测试优先: 先确保每个功能单元稳定。
- 集成测试补充: 验证整体流程时,再引入集成测试。
- Mock 外部依赖: 在集成测试中尽量减少对真实资源(数据库、网络)的依赖。
什么是项目的测试覆盖率?
测试覆盖率(Test Coverage)是衡量一个项目中有多少代码被测试用例覆盖的指标。它表示项目代码的质量保证程度。测试覆盖率通常以百分比的形式表示,如 80% 表示代码中 80% 的部分已经被测试用例运行过。
覆盖率分类
-
行覆盖率(Line Coverage)
检测每一行代码是否被执行。
-
分支覆盖率(Branch Coverage)
检测代码中的条件语句(如 if-else)的所有分支是否都被测试。
-
函数覆盖率(Function Coverage)
检测所有函数是否被调用。
-
路径覆盖率(Path Coverage)
检测所有可能的执行路径是否都被测试。
为什么测试覆盖率重要?
- 质量保证:确保关键代码路径经过充分测试。
- 维护性:发现未被测试的代码,优化测试用例。
- 团队规范:强制要求开发者在提交代码前编写测试。
如何计算测试覆盖率?
工具
在 Python 项目中,通常使用以下工具计算测试覆盖率:
- pytest-cov :配合
pytest
使用,易于集成。 - Coverage.py:独立的覆盖率工具,可生成详细的覆盖率报告。
- Codecov 和 Coveralls:托管服务,用于在 GitHub 等平台展示测试覆盖率。
在 GitHub 上展示测试覆盖率
许多开源项目在 GitHub 上会显示覆盖率指标,通过徽章(Badge)的形式展示,通常借助 Codecov 或 Coveralls 服务实现。
如何在 GitHub 项目中添加测试覆盖率?
1. 安装依赖
确保已安装以下工具:
bash
pip install pytest pytest-cov
pip install codecov
2. 配置 pytest-cov
在项目中运行测试并生成覆盖率报告:
bash
pytest --cov=my_project --cov-report=xml
这将生成一个 coverage.xml
文件,供上传到 Codecov 或其他服务。
3. 集成 Codecov
(1)登录 Codecov 并连接你的 GitHub 项目。
(2)在项目根目录添加一个 .github/workflows/codecov.yml
文件:
yaml
name: CI
on:
push:
branches:
- main
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.9'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov codecov
- name: Run tests with coverage
run: |
pytest --cov=my_project
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
(3)提交后,GitHub Actions 会自动运行测试并上传覆盖率到 Codecov。
4. 添加徽章
在 Codecov 项目的设置中获取徽章链接,将其添加到你的 README.md
文件中,例如:
markdown
[![codecov](https://codecov.io/gh/<username>/<repo>/branch/main/graph/badge.svg)](https://codecov.io/gh/<username>/<repo>)
覆盖率目标
-
行业标准:
- 一般项目:60%-80% 及格。
- 关键项目:95%+(例如金融系统、医疗系统)。
-
不能盲目追求100% :覆盖率高不一定代表没有 bug,关注测试的质量比单纯提高覆盖率更重要。
通过这些步骤,你的项目可以在 GitHub 上显示测试覆盖率,并增强项目的专业性和可信度!