Python单元测试与pytest框架
Python单元测试与pytest框架
一、为什么需要单元测试
单元测试是软件开发中的重要实践,它可以:
- 验证代码的正确性
- 防止回归错误
- 提高代码质量
- 作为代码文档
- 促进更好的设计
二、unittest基础
Python内置的unittest模块提供了基本的测试框架。
2.1 基本测试用例
import unittest
def add(a, b):
return a + b
class 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)
@classmethod
def setUpClass(cls):
"""所有测试前执行一次"""
print("设置测试类")
@classmethod
def tearDownClass(cls):
"""所有测试后执行一次"""
print("清理测试类")
四、pytest入门
pytest是更强大、更灵活的测试框架。
4.1 基本测试
# test_math.py
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
def test_add_negative():
assert add(-1, -1) == -2
# 运行: pytest test_math.py
4.2 pytest的优势
- 使用简单的assert语句
- 自动发现测试
- 丰富的插件生态
- 更好的错误报告
- 支持参数化测试
五、pytest夹具
5.1 基本夹具
import pytest
@pytest.fixture
def sample_data():
"""提供测试数据"""
return [1, 2, 3, 4, 5]
def test_sum(sample_data):
assert sum(sample_data) == 15
def test_length(sample_data):
assert len(sample_data) == 5
5.2 夹具作用域
@pytest.fixture(scope='function') # 默认,每个测试函数
def func_fixture():
return "function"
@pytest.fixture(scope='class') # 每个测试类
def class_fixture():
return "class"
@pytest.fixture(scope='module') # 每个模块
def module_fixture():
return "module"
@pytest.fixture(scope='session') # 整个测试会话
def session_fixture():
return "session"
5.3 夹具的setup和teardown
@pytest.fixture
def database():
# Setup
db = Database()
db.connect()
print("数据库已连接")
yield db # 提供给测试
# Teardown
db.disconnect()
print("数据库已断开")
def test_query(database):
result = database.query("SELECT * FROM users")
assert len(result) > 0
六、参数化测试
6.1 使用@pytest.mark.parametrize
import pytest
@pytest.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) == expected
6.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 pytest
@pytest.mark.skip(reason="暂时跳过")
def test_not_ready():
pass
@pytest.mark.skipif(sys.version_info < (3, 8), reason="需要Python 3.8+")
def test_new_feature():
pass
7.2 预期失败
@pytest.mark.xfail(reason="已知bug")
def test_known_bug():
assert 1 == 2
7.3 自定义标记
# pytest.ini
[pytest]
markers =
slow: 标记慢速测试
integration: 标记集成测试
# 使用标记
@pytest.mark.slow
def test_slow_operation():
time.sleep(5)
@pytest.mark.integration
def test_api_integration():
pass
# 运行特定标记的测试
# pytest -m slow
# pytest -m "not slow"
八、Mock和Patch
8.1 使用unittest.mock
from unittest.mock import Mock, patch
def get_user_data(user_id):
# 假设这会调用外部API
response = requests.get(f'https://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'] == 'Alice'
mock_get.assert_called_once_with('https://api.example.com/users/1')
8.2 Mock对象
def test_mock_object():
mock = Mock()
# 设置返回值
mock.method.return_value = 42
assert 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 --cov=myproject tests/
# 生成HTML报告
# pytest --cov=myproject --cov-report=html tests/
9.2 配置覆盖率
# .coveragerc
[run]
source = myproject
omit =
*/tests/*
*/venv/*
[report]
exclude_lines =
pragma: no cover
def __repr__
raise NotImplementedError
十、测试异常
10.1 使用pytest.raises
def divide(a, b):
if b == 0:
raise ValueError("除数不能为0")
return a / b
def 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, match=r"除数不能为0"):
divide(10, 0)
十一、测试组织
11.1 目录结构
project/
├── myproject/
│ ├── __init__.py
│ ├── module1.py
│ └── module2.py
└── tests/
├── __init__.py
├── test_module1.py
└── test_module2.py
11.2 conftest.py
conftest.py用于共享夹具和配置。
# tests/conftest.py
import pytest
@pytest.fixture
def sample_user():
return {'id': 1, 'name': 'Alice'}
# 所有测试文件都可以使用这个夹具
十二、实战案例:测试Web应用
# app.py
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/api/users/')
def get_user(user_id):
# 假设从数据库获取
user = {'id': user_id, 'name': 'Alice'}
return jsonify(user)
# test_app.py
import pytest
from app import app
@pytest.fixture
def client():
app.config['TESTING'] = True
with app.test_client() as client:
yield client
def test_get_user(client):
response = client.get('/api/users/1')
assert response.status_code == 200
data = response.get_json()
assert data['id'] == 1
十三、实战案例:测试数据库操作
import pytest
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
@pytest.fixture(scope='function')
def db_session():
# 使用内存数据库
engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
yield session
session.close()
def test_create_user(db_session):
user = User(name='Alice', email='alice@example.com')
db_session.add(user)
db_session.commit()
assert user.id is not None
assert db_session.query(User).count() == 1
十四、测试最佳实践
1. 测试应该独立且可重复
2. 一个测试只测试一个功能点
3. 使用描述性的测试名称
4. 遵循AAA模式:Arrange(准备)、Act(执行)、Assert(断言)
5. 不要测试实现细节,测试行为
6. 使用夹具避免重复代码
7. 保持测试简单易懂
8. 定期运行测试
9. 追求高覆盖率,但不要为了覆盖率而测试
十五、pytest配置
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts = -v --strict-markers --tb=short
十六、持续集成
# .github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install pytest pytest-cov
- name: Run tests
run: pytest --cov=myproject tests/
十七、总结
单元测试是保证代码质量的重要手段。pytest提供了强大而灵活的测试框架,通过夹具、参数化、标记等特性,可以编写清晰、可维护的测试代码。结合Mock、覆盖率工具和持续集成,可以构建完整的测试体系。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)