当前位置: 首页 > news >正文

Python单元测试:pytest最佳实践

Python单元测试:pytest最佳实践

引言

单元测试是保证代码质量的关键环节,它帮助我们在开发过程中快速发现问题。作为一名从Python转向Rust的后端开发者,我在实践中总结了单元测试的最佳实践。本文将深入探讨pytest框架的使用,帮助你构建高质量的测试套件。

一、单元测试基础概念

1.1 什么是单元测试

单元测试是对软件中最小可测试单元进行验证的测试方法。

1.2 单元测试的重要性

  • 快速反馈:在开发过程中立即发现问题
  • 文档作用:测试用例是代码的活文档
  • 重构支持:安全地进行代码重构
  • 质量保障:防止回归问题

1.3 测试金字塔

单元测试(底层)- 数量最多 | 集成测试(中层)- 数量适中 | 端到端测试(顶层)- 数量最少

二、pytest入门

2.1 安装pytest

pip install pytest pytest-cov pytest-mock

2.2 基本测试结构

# test_example.py def test_addition(): assert 1 + 1 == 2 def test_string_length(): assert len("hello") == 5 def test_list_contains(): assert 3 in [1, 2, 3, 4, 5]

2.3 运行测试

pytest test_example.py -v pytest tests/ -v --tb=short pytest -x # 遇到第一个失败就停止 pytest --tb=no # 不显示traceback

三、测试用例组织

3.1 测试类

class TestCalculator: def test_add(self): assert 2 + 3 == 5 def test_subtract(self): assert 5 - 3 == 2 def test_multiply(self): assert 4 * 5 == 20 def test_divide(self): assert 10 / 2 == 5

3.2 参数化测试

import pytest @pytest.mark.parametrize("a, b, expected", [ (1, 2, 3), (10, 20, 30), (-1, 1, 0), (0, 0, 0), ]) def test_add(a, b, expected): assert a + b == expected @pytest.mark.parametrize("input_value, expected", [ ("hello", 5), ("", 0), ("python", 6), ]) def test_string_length(input_value, expected): assert len(input_value) == expected

3.3 测试文件结构

project/ ├── src/ │ └── my_module/ │ ├── __init__.py │ └── calculator.py └── tests/ ├── __init__.py ├── conftest.py └── test_calculator.py

四、fixture机制

4.1 基本fixture

# conftest.py import pytest @pytest.fixture def database_connection(): conn = create_connection() yield conn conn.close() @pytest.fixture def test_user(): return {"id": 1, "name": "Test User"}

4.2 使用fixture

def test_database_query(database_connection, test_user): result = database_connection.query("SELECT * FROM users WHERE id = %s", test_user["id"]) assert result["name"] == test_user["name"]

4.3 fixture作用域

@pytest.fixture(scope="module") def module_fixture(): print("Module setup") yield print("Module teardown") @pytest.fixture(scope="session") def session_fixture(): print("Session setup") yield print("Session teardown")

4.4 参数化fixture

@pytest.fixture(params=["sqlite", "postgresql", "mysql"]) def database_type(request): return request.param def test_database_connection(database_type): assert database_type in ["sqlite", "postgresql", "mysql"]

五、mock与monkey patch

5.1 使用unittest.mock

from unittest.mock import Mock, patch def test_api_call(): mock_response = Mock() mock_response.json.return_value = {"status": "success"} with patch("requests.get", return_value=mock_response): result = make_api_call("http://example.com") assert result == {"status": "success"} requests.get.assert_called_once_with("http://example.com")

5.2 使用pytest-mock

def test_with_mock(mocker): mock_function = mocker.patch("my_module.my_function") mock_function.return_value = "mocked result" result = my_module.use_my_function() assert result == "mocked result" mock_function.assert_called_once()

5.3 模拟类方法

def test_class_method_mock(mocker): mock_method = mocker.patch.object(MyClass, "method") mock_method.return_value = 42 obj = MyClass() result = obj.method() assert result == 42 mock_method.assert_called_once()

六、测试断言

6.1 基本断言

assert value == expected assert value != expected assert value is None assert value is not None assert value in container assert value not in container assert condition

6.2 异常断言

def test_divide_by_zero(): with pytest.raises(ZeroDivisionError): 1 / 0 def test_exception_message(): with pytest.raises(ValueError, match="invalid value"): raise ValueError("This is an invalid value")

6.3 警告断言

def test_deprecation_warning(): with pytest.warns(DeprecationWarning): deprecated_function()

七、测试配置

7.1 pytest.ini配置

[pytest] testpaths = ["tests"] python_files = test_*.py python_classes = Test* python_functions = test_* addopts = -v --tb=short

7.2 setup.cfg配置

[tool:pytest] testpaths = ["tests"] addopts = --cov=src --cov-report=term-missing

7.3 tox配置

[tox] envlist = py38, py39, py310 skipsdist = true [testenv] deps = pytest pytest-cov pytest-mock commands = pytest tests/ --cov=src

八、测试覆盖率

8.1 生成覆盖率报告

pytest --cov=src --cov-report=html --cov-report=term

8.2 配置覆盖率阈值

[pytest] addopts = --cov=src --cov-report=term --cov-fail-under=80

8.3 排除文件

# .coveragerc [run] omit = src/__init__.py src/config.py

九、测试最佳实践

9.1 测试命名规范

def test_function_name_should_do_something(): pass def test_widget_when_scenario_then_result(): pass

9.2 AAA模式

def test_calculator_add(): # Arrange calculator = Calculator() # Act result = calculator.add(2, 3) # Assert assert result == 5

9.3 隔离性原则

def test_database_operations(): # 每个测试用例应该独立 setup_test_database() # 执行测试 result = perform_operation() assert result is not None # 清理 teardown_test_database()

9.4 避免测试逻辑

# 不好的做法 def test_complex_logic(): if condition: assert something else: assert something_else # 好的做法 def test_case_one(): assert something def test_case_two(): assert something_else

十、实战案例:完整测试套件

import pytest from unittest.mock import patch, MagicMock from my_app import UserService, Database, Cache @pytest.fixture def mock_database(): return MagicMock(spec=Database) @pytest.fixture def mock_cache(): return MagicMock(spec=Cache) @pytest.fixture def user_service(mock_database, mock_cache): return UserService(database=mock_database, cache=mock_cache) @pytest.mark.parametrize("user_id, expected_name", [ (1, "Alice"), (2, "Bob"), (3, "Charlie"), ]) def test_get_user(user_service, mock_database, user_id, expected_name): mock_database.query.return_value = {"id": user_id, "name": expected_name} user = user_service.get_user(user_id) assert user["id"] == user_id assert user["name"] == expected_name mock_database.query.assert_called_once_with("SELECT * FROM users WHERE id = %s", user_id) def test_get_user_cached(user_service, mock_database, mock_cache): mock_cache.get.return_value = {"id": 1, "name": "Cached Alice"} user = user_service.get_user(1) assert user["name"] == "Cached Alice" mock_cache.get.assert_called_once_with("user:1") mock_database.query.assert_not_called() def test_create_user_validation(user_service, mock_database): with pytest.raises(ValueError, match="Name is required"): user_service.create_user(name="") def test_create_user_success(user_service, mock_database): mock_database.insert.return_value = 42 user_id = user_service.create_user(name="New User") assert user_id == 42 mock_database.insert.assert_called_once_with( "INSERT INTO users (name) VALUES (%s)", "New User" ) @patch("my_app.external_api.send_email") def test_notify_user(mock_send_email, user_service): user = {"id": 1, "email": "user@example.com"} user_service.notify_user(user) mock_send_email.assert_called_once_with( to="user@example.com", subject="Welcome", body="Hello, welcome to our service!" )

总结

单元测试是保证代码质量的关键环节。通过本文的学习,你应该掌握了以下核心要点:

  1. 单元测试基础:测试金字塔、重要性
  2. pytest入门:安装、基本结构、运行测试
  3. 测试组织:测试类、参数化测试
  4. fixture机制:基本fixture、作用域、参数化
  5. mock技术:unittest.mock、pytest-mock
  6. 断言技巧:基本断言、异常断言、警告断言
  7. 测试配置:pytest.ini、setup.cfg、tox
  8. 测试覆盖率:生成报告、阈值配置
  9. 最佳实践:命名规范、AAA模式、隔离性原则
  10. 实战案例:完整的测试套件

作为从Python转向Rust的后端开发者,掌握单元测试对于构建高质量系统至关重要。后续文章将深入探讨Rust中的测试框架与模式。

http://www.jsqmd.com/news/906352/

相关文章:

  • 在自动化脚本中集成 Taotoken API 实现定时任务与批量处理
  • 2026年前置过滤器厂家TOP5测评:成都0阻垢剂净水器、成都全屋净水、成都净水器、成都净水器买哪款、成都净水器推荐选择指南 - 优质品牌商家
  • 猫抓浏览器扩展:5分钟掌握视频资源嗅探下载技巧
  • 2026年5月北京别墅装修公司推荐:TOP5排名专业评测大宅空间设计价格 - 品牌推荐
  • 别再只用QPushButton了!Qt Creator里这个隐藏的‘小工具’QToolButton,让你的工具栏更专业
  • 2026年5月天津除甲醛公司推荐:五大专业排行评测母婴级安全价格注意事项 - 品牌推荐
  • 2026年锦城学院深度盘点:高教场景应用型人才培养与就业痛点破解 - 品牌推荐
  • 2026年5月市面上除甲醛公司哪家权威厂家推荐榜,光触媒、生物酶、CMA检测三大主流类型厂家选择指南 - 海棠依旧大
  • 扎克伯格夫妇旗下Biohub发布蛋白质“世界模型“
  • 从零开始:用NEURON和Python搭建你的第一个神经元模型(保姆级教程)
  • 2026年5月比较好的安阳婚姻服务服务中心推荐榜厂家推荐榜,一对一婚恋匹配/高端猎婚定制/中老年婚恋关怀/婚恋心理辅导/企业联谊活动厂家选择指南 - 海棠依旧大
  • 别再只用总线了!OrCAD 16.6的NetGroup功能,帮你搞定原理图里那些“散装”信号
  • 044、手持视频抖动严重?OpenCV 光流 + IMU 融合的电子防抖工程方案
  • 流式输出(Streaming):为你的自研 AI 平台添加类似打字机的体验
  • AI Agent从入门到精通:收藏这份保姆级指南,小白也能轻松掌握智能代理核心技术!
  • 2026年成都锦城学院深度解析:民办高校志愿填报场景信息不对称与择校焦虑 - 品牌推荐
  • Python异步IO:asyncio深度解析
  • 成都收账公司实测评测:成都正规收账公司有哪些/成都调查公司/成都调查公司电话/成都靠谱寻人寻车寻物公司/靠谱的调查公司/选择指南 - 优质品牌商家
  • 别再被MOS管炸了!手把手教你设计栅极驱动电路(附TVS管和电阻选型)
  • Dotween动画控制避坑指南:从播放、暂停到倒放,这些细节新手容易忽略
  • 2026年成都锦城学院深度解析:民办高校招生竞争加剧下的品牌突围与质量保障 - 品牌推荐
  • 045、视频慢动作生成卡顿?RIFE/DAIN 插帧模型选型与 GPU 推理加速方案
  • 从‘像素级’到‘结构感知’:手把手教你用NumPy实现SSIM算法,彻底搞懂它为什么比MSE/PSNR更合理
  • 成本控制必修课:如何在代码中精确计算并限制 LLM 的 Token 消耗?
  • Rust分布式追踪:构建可观测的微服务系统
  • 2026年锦城学院深度解析:民办高校选校场景信息不对称与择校迷茫 - 品牌推荐
  • 2026年锦城学院深度解析:民办高校选择中信息不对称与信任焦虑 - 品牌推荐
  • 别再只用TeamViewer了!用WOL+远程桌面,打造你的24小时待命个人云电脑
  • 啤酒厂建设工程技术要点与主流厂家选型参考:现代化啤酒厂建设、精酿啤酒投资、精酿啤酒设备、自酿啤酒设备、鲜啤酿酒设备选择指南 - 优质品牌商家
  • LaserGRBL:5个步骤掌握免费激光雕刻控制软件的终极指南