Python 单元测试
单元测试是软件开发中确保代码质量的重要环节。本章节详细介绍Python单元测试的各个方面,包括unittest和pytest框架、测试编写技巧、模拟(mocking)以及测试覆盖率分析。
1. 单元测试基础
1.1 什么是单元测试?
单元测试是指对软件中最小可测试单元(如函数或方法)进行验证,以确保其按预期工作。单元测试通常是自动化的,运行快速且独立于其他代码。
1.2 为什么需要单元测试?
- 确保代码正确性:验证代码是否按预期运行。
- 便于重构:在修改代码时,快速检查是否引入新错误。
- 提高代码质量:通过测试驱动开发(TDD),编写更可靠的代码。
- 文档化:测试用例可以作为代码功能的示例。
2. 使用unittest
框架
Python内置的unittest
模块是学习单元测试的起点,适合理解测试的基本结构。
2.1 编写第一个单元测试
假设我们有一个简单的函数,用于计算两个数的和。我们将为其编写测试。
# calculator.py
def add(a, b):
return a + b
以下是对应的单元测试代码:
# test_calculator_unittest.py
import unittest
from calculator import add
class TestCalculator(unittest.TestCase):
def test_add_positive_numbers(self):
result = add(2, 3)
self.assertEqual(result, 5, "2 + 3 should equal 5")
def test_add_negative_numbers(self):
result = add(-1, -2)
self.assertEqual(result, -3, "-1 + -2 should equal -3")
def test_add_zero(self):
result = add(5, 0)
self.assertEqual(result, 5, "5 + 0 should equal 5")
if __name__ == '__main__':
unittest.main()
运行测试:python -m unittest test_calculator_unittest.py
。unittest.TestCase
提供了多种断言方法,如assertEqual
、assertTrue
等,用于验证结果。
2.2 unittest
常用断言
assertEqual(a, b)
:检查a == b
。assertTrue(x)
:检查x
为真。assertRaises(Exception, func, *args)
:检查函数是否抛出指定异常。
示例:测试抛出异常的场景。
# calculator.py
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
# test_calculator_unittest.py
import unittest
from calculator import divide
class TestCalculator(unittest.TestCase):
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
divide(10, 0)
def test_divide_positive(self):
result = divide(10, 2)
self.assertEqual(result, 5.0, "10 / 2 should equal 5.0")
if __name__ == '__main__':
unittest.main()
3. 使用pytest
框架
pytest
是一个功能强大且更简洁的测试框架,广泛用于Python项目。它支持更自然的测试编写方式,且有丰富的插件生态。
3.1 安装pytest
运行以下命令安装pytest
:
pip install pytest
3.2 编写pytest
测试
使用相同的calculator.py
,以下是pytest
风格的测试代码:
# test_calculator_pytest.py
from calculator import add
def test_add_positive_numbers():
assert add(2, 3) == 5, "2 + 3 should equal 5"
def test_add_negative_numbers():
assert add(-1, -2) == -3, "-1 + -2 should equal -3"
def test_add_zero():
assert add(5, 0) == 5, "5 + 0 should equal 5"
运行测试:pytest test_calculator_pytest.py
。pytest
会自动发现以test_
开头的文件和函数,执行测试并提供详细的输出。
3.3 测试异常
pytest
使用pytest.raises
来测试异常:
# test_calculator_pytest.py
import pytest
from calculator import divide
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
def test_divide_positive():
assert divide(10, 2) == 5.0, "10 / 2 should equal 5.0"
运行:pytest test_calculator_pytest.py
。
4. 模拟(Mocking)测试
对于依赖外部资源(如数据库、API)的代码,单元测试需要隔离这些依赖。Python的unittest.mock
模块提供了模拟功能。
4.1 使用unittest.mock
假设有一个函数调用外部API获取数据:
# api_client.py
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
以下是使用mock
的测试代码:
# test_api_client.py
import unittest
from unittest.mock import patch
from api_client import get_user_data
class TestApiClient(unittest.TestCase):
@patch('requests.get')
def test_get_user_data(self, mock_get):
mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
result = get_user_data(1)
self.assertEqual(result, {"id": 1, "name": "Alice"})
mock_get.assert_called_once_with("https://api.example.com/users/1")
if __name__ == '__main__':
unittest.main()
@patch
装饰器模拟了requests.get
,避免实际的网络请求。
4.2 使用pytest
和pytest-mock
pytest-mock
是pytest
的插件,简化了模拟操作。安装:pip install pytest-mock
。
# test_api_client_pytest.py
from api_client import get_user_data
def test_get_user_data(mocker):
mock_get = mocker.patch('api_client.requests.get')
mock_get.return_value.json.return_value = {"id": 1, "name": "Alice"}
result = get_user_data(1)
assert result == {"id": 1, "name": "Alice"}
mock_get.assert_called_once_with("https://api.example.com/users/1")
运行:pytest test_api_client_pytest.py
。
5. 测试覆盖率
测试覆盖率用于衡量代码被测试的程度。可以使用pytest-cov
插件。
5.1 安装和使用
安装:pip install pytest-cov
。
运行测试并生成覆盖率报告:
pytest --cov=calculator test_calculator_pytest.py
示例报告:
Name Stmts Miss Cover
------------------------------------
calculator.py 5 0 100%
------------------------------------
TOTAL 5 0 100%
5.2 提高覆盖率
确保测试覆盖所有代码路径,包括异常情况和边缘案例。例如,扩展divide
函数的测试:
# test_calculator_pytest.py
import pytest
from calculator import divide
def test_divide_by_zero():
with pytest.raises(ValueError, match="Cannot divide by zero"):
divide(10, 0)
def test_divide_positive():
assert divide(10, 2) == 5.0, "10 / 2 should equal 5.0"
def test_divide_negative():
assert divide(-10, 2) == -5.0, "-10 / 2 should equal -5.0"
def test_divide_fraction():
assert divide(1, 2) == 0.5, "1 / 2 should equal 0.5"
6. 测试最佳实践
- 单一职责:每个测试用例测试一个功能。
- 命名清晰:测试函数名应描述测试内容,如
test_add_negative_numbers
。 - 隔离性:测试不应依赖外部状态或彼此。
- 覆盖边缘情况:测试异常、边界值和特殊输入。
- 使用TDD:先写测试,再写代码,确保代码满足测试要求。