Python单元测试与pytest框架
Python单元测试与pytest框架一、为什么需要单元测试单元测试是软件开发中的重要实践它可以- 验证代码的正确性- 防止回归错误- 提高代码质量- 作为代码文档- 促进更好的设计二、unittest基础Python内置的unittest模块提供了基本的测试框架。2.1 基本测试用例import unittestdef add(a, b):return a bclass TestMath(unittest.TestCase):def test_add_positive(self):self.assertEqual(add(2, 3), 5)def test_add_negative(self):self.assertEqual(add(-1, -1), -2)def test_add_zero(self):self.assertEqual(add(5, 0), 5)if __name__ __main__:unittest.main()2.2 常用断言方法class TestAssertions(unittest.TestCase):def test_equality(self):self.assertEqual(1 1, 2)self.assertNotEqual(1, 2)def test_boolean(self):self.assertTrue(True)self.assertFalse(False)def test_none(self):self.assertIsNone(None)self.assertIsNotNone(1)def test_membership(self):self.assertIn(1, [1, 2, 3])self.assertNotIn(4, [1, 2, 3])def test_exceptions(self):with self.assertRaises(ValueError):int(invalid)def test_almost_equal(self):self.assertAlmostEqual(0.1 0.2, 0.3)三、测试夹具Fixtures测试夹具用于设置和清理测试环境。class TestDatabase(unittest.TestCase):def setUp(self):每个测试方法前执行self.db Database()self.db.connect()def tearDown(self):每个测试方法后执行self.db.disconnect()def test_insert(self):self.db.insert(test)self.assertEqual(self.db.count(), 1)def test_delete(self):self.db.insert(test)self.db.delete(test)self.assertEqual(self.db.count(), 0)classmethoddef setUpClass(cls):所有测试前执行一次print(设置测试类)classmethoddef tearDownClass(cls):所有测试后执行一次print(清理测试类)四、pytest入门pytest是更强大、更灵活的测试框架。4.1 基本测试# test_math.pydef add(a, b):return a bdef test_add():assert add(2, 3) 5def test_add_negative():assert add(-1, -1) -2# 运行: pytest test_math.py4.2 pytest的优势- 使用简单的assert语句- 自动发现测试- 丰富的插件生态- 更好的错误报告- 支持参数化测试五、pytest夹具5.1 基本夹具import pytestpytest.fixturedef sample_data():提供测试数据return [1, 2, 3, 4, 5]def test_sum(sample_data):assert sum(sample_data) 15def test_length(sample_data):assert len(sample_data) 55.2 夹具作用域pytest.fixture(scopefunction) # 默认每个测试函数def func_fixture():return functionpytest.fixture(scopeclass) # 每个测试类def class_fixture():return classpytest.fixture(scopemodule) # 每个模块def module_fixture():return modulepytest.fixture(scopesession) # 整个测试会话def session_fixture():return session5.3 夹具的setup和teardownpytest.fixturedef database():# Setupdb Database()db.connect()print(数据库已连接)yield db # 提供给测试# Teardowndb.disconnect()print(数据库已断开)def test_query(database):result database.query(SELECT * FROM users)assert len(result) 0六、参数化测试6.1 使用pytest.mark.parametrizeimport pytestpytest.mark.parametrize(a, b, expected, [(2, 3, 5),(0, 0, 0),(-1, 1, 0),(10, -5, 5),])def test_add(a, b, expected):assert add(a, b) expected6.2 多个参数组合pytest.mark.parametrize(x, [1, 2, 3])pytest.mark.parametrize(y, [10, 20])def test_multiply(x, y):assert x * y y * x# 会生成6个测试(1,10), (1,20), (2,10), (2,20), (3,10), (3,20)七、测试标记7.1 跳过测试import pytestpytest.mark.skip(reason暂时跳过)def test_not_ready():passpytest.mark.skipif(sys.version_info (3, 8), reason需要Python 3.8)def test_new_feature():pass7.2 预期失败pytest.mark.xfail(reason已知bug)def test_known_bug():assert 1 27.3 自定义标记# pytest.ini[pytest]markers slow: 标记慢速测试integration: 标记集成测试# 使用标记pytest.mark.slowdef test_slow_operation():time.sleep(5)pytest.mark.integrationdef test_api_integration():pass# 运行特定标记的测试# pytest -m slow# pytest -m not slow八、Mock和Patch8.1 使用unittest.mockfrom unittest.mock import Mock, patchdef get_user_data(user_id):# 假设这会调用外部APIresponse requests.get(fhttps://api.example.com/users/{user_id})return response.json()def test_get_user_data():with patch(requests.get) as mock_get:# 配置mock返回值mock_get.return_value.json.return_value {id: 1, name: Alice}result get_user_data(1)assert result[name] Alicemock_get.assert_called_once_with(https://api.example.com/users/1)8.2 Mock对象def test_mock_object():mock Mock()# 设置返回值mock.method.return_value 42assert mock.method() 42# 设置副作用mock.method.side_effect ValueError(错误)with pytest.raises(ValueError):mock.method()# 验证调用mock.method.assert_called()mock.method.assert_called_with()8.3 pytest-mock插件def test_with_mocker(mocker):# mocker是pytest-mock提供的夹具mock_get mocker.patch(requests.get)mock_get.return_value.json.return_value {data: test}result get_user_data(1)assert result[data] test九、测试覆盖率9.1 使用pytest-cov# 安装: pip install pytest-cov# 运行测试并生成覆盖率报告# pytest --covmyproject tests/# 生成HTML报告# pytest --covmyproject --cov-reporthtml tests/9.2 配置覆盖率# .coveragerc[run]source myprojectomit */tests/**/venv/*[report]exclude_lines pragma: no coverdef __repr__raise NotImplementedError十、测试异常10.1 使用pytest.raisesdef divide(a, b):if b 0:raise ValueError(除数不能为0)return a / bdef test_divide_by_zero():with pytest.raises(ValueError) as exc_info:divide(10, 0)assert 除数不能为0 in str(exc_info.value)10.2 测试异常消息def test_exception_message():with pytest.raises(ValueError, matchr除数不能为0):divide(10, 0)十一、测试组织11.1 目录结构project/├── myproject/│ ├── __init__.py│ ├── module1.py│ └── module2.py└── tests/├── __init__.py├── test_module1.py└── test_module2.py11.2 conftest.pyconftest.py用于共享夹具和配置。# tests/conftest.pyimport pytestpytest.fixturedef sample_user():return {id: 1, name: Alice}# 所有测试文件都可以使用这个夹具十二、实战案例测试Web应用# app.pyfrom flask import Flask, jsonifyapp Flask(__name__)app.route(/api/users/)def get_user(user_id):# 假设从数据库获取user {id: user_id, name: Alice}return jsonify(user)# test_app.pyimport pytestfrom app import apppytest.fixturedef client():app.config[TESTING] Truewith app.test_client() as client:yield clientdef test_get_user(client):response client.get(/api/users/1)assert response.status_code 200data response.get_json()assert data[id] 1十三、实战案例测试数据库操作import pytestfrom sqlalchemy import create_enginefrom sqlalchemy.orm import sessionmakerpytest.fixture(scopefunction)def db_session():# 使用内存数据库engine create_engine(sqlite:///:memory:)Base.metadata.create_all(engine)Session sessionmaker(bindengine)session Session()yield sessionsession.close()def test_create_user(db_session):user User(nameAlice, emailaliceexample.com)db_session.add(user)db_session.commit()assert user.id is not Noneassert db_session.query(User).count() 1十四、测试最佳实践1. 测试应该独立且可重复2. 一个测试只测试一个功能点3. 使用描述性的测试名称4. 遵循AAA模式Arrange准备、Act执行、Assert断言5. 不要测试实现细节测试行为6. 使用夹具避免重复代码7. 保持测试简单易懂8. 定期运行测试9. 追求高覆盖率但不要为了覆盖率而测试十五、pytest配置# pytest.ini[pytest]testpaths testspython_files test_*.pypython_classes Test*python_functions test_*addopts -v --strict-markers --tbshort十六、持续集成# .github/workflows/test.ymlname: Testson: [push, pull_request]jobs:test:runs-on: ubuntu-lateststeps:- uses: actions/checkoutv2- name: Set up Pythonuses: actions/setup-pythonv2with:python-version: 3.9- name: Install dependenciesrun: |pip install -r requirements.txtpip install pytest pytest-cov- name: Run testsrun: pytest --covmyproject tests/十七、总结单元测试是保证代码质量的重要手段。pytest提供了强大而灵活的测试框架通过夹具、参数化、标记等特性可以编写清晰、可维护的测试代码。结合Mock、覆盖率工具和持续集成可以构建完整的测试体系。