1. 单元测试
概念
- 定义: 单元测试是对代码中最小功能单元的测试,通常是函数或类的方法。
- 目标: 验证单个功能是否按照预期工作,而不依赖其他模块或外部资源。
- 特点: 快速、独立,通常是开发者最先编写的测试。
示例:pytest 实现单元测试
# 功能模块:一个简单的数学函数
def add(x, y):"""加法函数"""return x + ydef 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) # 测试除零异常
执行命令
运行单元测试:
pytest test_example.py
优点:
- 快速反馈代码问题。
- 单一功能模块的高覆盖率。
2. 集成测试
概念
- 定义: 集成测试是验证多个模块的交互行为是否正常,确保它们组合在一起能够按预期工作。
- 目标: 检查模块之间的接口和协作行为,可能涉及数据库、API 或文件系统等外部依赖。
- 特点: 比单元测试慢,但更贴近实际场景。
示例:pytest 实现集成测试
使用数据库模拟的场景
假设我们有一个用户管理模块,需要测试用户的创建、查询和删除功能:
# 功能模块:用户管理
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] = emaildef 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")
执行命令
运行集成测试:
pytest test_example.py
单元测试与集成测试的区别
特性 | 单元测试 | 集成测试 |
---|---|---|
测试范围 | 单一模块或函数 | 多个模块之间的交互 |
目标 | 验证单独功能是否正确 | 验证整体功能是否按预期工作 |
速度 | 快速 | 较慢 |
复杂度 | 较低 | 较高,可能涉及外部依赖 |
测试工具 | 模拟对象 (Mock) | 实际环境或部分模拟环境 |
3. pytest 中的 Mock 模拟(用于集成测试中的外部依赖)
在集成测试中,我们可能需要模拟外部依赖(如数据库、API)。pytest
支持使用 unittest.mock
来实现 Mock。
示例:模拟外部 API
假设我们有一个函数需要从外部 API 获取数据:
# 功能模块:从外部 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
from unittest.mock import MagicMockdef test_fetch_data():"""测试 fetch_data 函数,使用 Mock 模拟 API 行为"""# 创建 Mock API 客户端mock_client = MagicMock()# 模拟成功响应mock_client.get.return_value.status_code = 200mock_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")
运行测试
使用以下命令运行测试:
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. 安装依赖
确保已安装以下工具:
pip install pytest pytest-cov
pip install codecov
2. 配置 pytest-cov
在项目中运行测试并生成覆盖率报告:
pytest --cov=my_project --cov-report=xml
这将生成一个 coverage.xml
文件,供上传到 Codecov 或其他服务。
3. 集成 Codecov
(1)登录 Codecov 并连接你的 GitHub 项目。
(2)在项目根目录添加一个 .github/workflows/codecov.yml
文件:
name: CIon:push:branches:- mainjobs:test:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v3- name: Set up Pythonuses: actions/setup-python@v4with:python-version: '3.9'- name: Install dependenciesrun: |python -m pip install --upgrade pippip install pytest pytest-cov codecov- name: Run tests with coveragerun: |pytest --cov=my_project- name: Upload coverage to Codecovuses: codecov/codecov-action@v3with:file: ./coverage.xml
(3)提交后,GitHub Actions 会自动运行测试并上传覆盖率到 Codecov。
4. 添加徽章
在 Codecov 项目的设置中获取徽章链接,将其添加到你的 README.md
文件中,例如:
[![codecov](https://codecov.io/gh/<username>/<repo>/branch/main/graph/badge.svg)](https://codecov.io/gh/<username>/<repo>)
覆盖率目标
-
行业标准:
- 一般项目:60%-80% 及格。
- 关键项目:95%+(例如金融系统、医疗系统)。
-
不能盲目追求100%:覆盖率高不一定代表没有 bug,关注测试的质量比单纯提高覆盖率更重要。
通过这些步骤,你的项目可以在 GitHub 上显示测试覆盖率,并增强项目的专业性和可信度!