你是否遇到过这样的场景?修改了一段代码后,原本正常的功能突然报错;上线前信心满满,却被测试同学用边界条件“吊打”;想重构旧代码,却因没有测试用例而战战兢兢……这些问题的根源,往往是缺乏有效的单元测试。
Python的unittest
框架(官方内置,无需额外安装)是单元测试的“瑞士军刀”,它提供了从测试用例编写、执行到报告生成的全流程支持。本文将通过计算器功能开发的完整案例,带你彻底掌握unittest
的核心用法。
单元测试(Unit Testing)是对程序最小可测试单元(如函数、方法)的验证,目标是确保每个“代码模块”单独工作正常。unittest
作为Python的标准测试框架,具备以下优势:
在正式编码前,先明确unittest
的关键术语:
术语 | 定义 |
---|---|
测试用例(Test Case) | 最小测试单元,通过unittest.TestCase 子类实现,包含单个或多个测试方法。 |
测试方法(Test Method) | 以test_ 开头的方法(如test_add() ),定义具体的测试逻辑。 |
断言(Assertion) | 验证实际结果与预期是否一致的方法(如self.assertEqual(a, b) )。 |
测试套件(Test Suite) | 测试用例/测试套件的集合,用于批量执行测试(如按模块分组)。 |
测试运行器(Test Runner) | 执行测试并输出结果的组件(如命令行运行器、HTML报告生成器)。 |
setUp()/tearDown() | 测试方法的前置/后置操作(如初始化数据库连接、清理临时文件)。 |
假设我们要开发一个calculator.py
模块,包含加法、减法、除法三个核心方法。现在需要为其编写单元测试,确保功能正确性。
首先实现待测试的功能:
# calculator.py
class Calculator:
@staticmethod
def add(a, b):
return a + b
@staticmethod
def subtract(a, b):
return a - b
@staticmethod
def divide(a, b):
if b == 0:
raise ValueError("除数不能为0")
return a / b
测试用例需继承unittest.TestCase
,并定义以test_
开头的测试方法。以下是完整实现:
# test_calculator.py
import unittest
from calculator import Calculator
class TestCalculatorAdd(unittest.TestCase):
"""测试加法功能的测试用例"""
def test_add_positive_numbers(self):
"""测试两个正数相加"""
result = Calculator.add(3, 5)
self.assertEqual(result, 8) # 断言结果等于8
def test_add_negative_numbers(self):
"""测试两个负数相加"""
result = Calculator.add(-2, -4)
self.assertEqual(result, -6)
def test_add_mixed_signs(self):
"""测试正负混合相加"""
result = Calculator.add(7, -3)
self.assertEqual(result, 4)
class TestCalculatorSubtract(unittest.TestCase):
"""测试减法功能的测试用例"""
def test_subtract_positive(self):
"""测试正数减正数"""
result = Calculator.subtract(10, 4)
self.assertEqual(result, 6)
def test_subtract_negative(self):
"""测试正数减负数"""
result = Calculator.subtract(5, -2)
self.assertEqual(result, 7)
class TestCalculatorDivide(unittest.TestCase):
"""测试除法功能的测试用例"""
def setUp(self):
"""每个测试方法执行前的前置操作(如初始化数据)"""
self.valid_inputs = [(8, 2), (15, 5)] # 定义有效输入
self.invalid_input = (5, 0) # 定义无效输入(除数为0)
def test_divide_normal(self):
"""测试正常除法"""
for a, b in self.valid_inputs:
result = Calculator.divide(a, b)
self.assertEqual(result, a / b) # 断言结果等于预期值
def test_divide_by_zero(self):
"""测试除数为0的异常"""
with self.assertRaises(ValueError) as context: # 断言抛出指定异常
Calculator.divide(*self.invalid_input)
self.assertEqual(str(context.exception), "除数不能为0") # 断言异常信息正确
def tearDown(self):
"""每个测试方法执行后的清理操作(如关闭连接)"""
print(f"\n[tearDown] 完成测试:{self._testMethodName}") # 演示作用
if __name__ == '__main__':
unittest.main() # 运行当前文件中的所有测试用例
Test
开头+被测试功能名(如TestCalculatorAdd
),清晰表达测试范围;test_
开头+测试场景(如test_add_positive_numbers
),方便定位失败用例;self.assertEqual(a, b)
验证相等,self.assertRaises
验证异常,unittest
提供了40+种断言方法(如assertTrue
、assertIn
);@parameterized.expand
(需安装parameterized
库)实现更清晰的参数化:from parameterized import parameterized
class TestCalculatorAdd(unittest.TestCase):
@parameterized.expand([
(3, 5, 8), # 输入1:a=3, b=5, 预期=8
(-2, -4, -6), # 输入2:a=-2, b=-4, 预期=-6
(7, -3, 4), # 输入3:a=7, b=-3, 预期=4
])
def test_add(self, a, b, expected):
self.assertEqual(Calculator.add(a, b), expected)
方式1:直接运行测试文件
在test_calculator.py
末尾添加unittest.main()
,直接执行文件:
python test_calculator.py
方式2:命令行指定测试用例
通过unittest
模块直接运行,支持指定测试模块、类、方法:
# 运行所有测试
python -m unittest test_calculator.py
# 运行TestCalculatorDivide类的所有测试
python -m unittest test_calculator.TestCalculatorDivide
# 运行TestCalculatorDivide类的test_divide_by_zero方法
python -m unittest test_calculator.TestCalculatorDivide.test_divide_by_zero
方式3:测试发现(Test Discovery)
unittest
支持自动查找项目中的测试用例(需满足以下条件):
test_
开头;TestCase
;test_
开头。执行命令自动发现并运行所有测试:
python -m unittest discover -s ./tests -p "test_*.py" # 在./tests目录下查找所有test_*.py文件
运行测试后,控制台会输出类似以下结果:
.....
----------------------------------------------------------------------
Ran 5 tests in 0.001s
OK
.
表示测试通过;F
表示测试失败(断言不通过);E
表示测试出错(代码抛出未捕获异常);s
表示测试被跳过(通过@unittest.skip
装饰器)。失败示例(故意将add(3,5)
的预期结果改为9):
F....
======================================================================
FAIL: test_add_positive_numbers (test_calculator.TestCalculatorAdd)
测试两个正数相加
----------------------------------------------------------------------
Traceback (most recent call last):
File "test_calculator.py", line 7, in test_add_positive_numbers
self.assertEqual(result, 9)
AssertionError: 8 != 9
----------------------------------------------------------------------
Ran 5 tests in 0.001s
FAILED (failures=1)
unittest
默认输出控制台报告,适合开发阶段。若需更友好的报告(如HTML格式),可使用第三方库HTMLTestRunner
(需手动下载或通过pip install html-testRunner
安装)。
# test_report.py(需与测试用例同目录)
import unittest
from HtmlTestRunner import HTMLTestRunner
from test_calculator import * # 导入测试用例
# 创建测试套件
test_suite = unittest.TestSuite()
test_suite.addTests([
unittest.makeSuite(TestCalculatorAdd),
unittest.makeSuite(TestCalculatorSubtract),
unittest.makeSuite(TestCalculatorDivide)
])
# 配置报告生成参数
runner = HTMLTestRunner(
output="test_reports", # 报告输出目录
report_name="Calculator_Test_Report", # 报告名称
report_title="计算器功能测试报告", # 报告标题
combine_reports=True # 合并多个测试类的报告
)
# 运行测试并生成报告
runner.run(test_suite)
运行后,test_reports
目录会生成Calculator_Test_Report.html
文件,包含:
通过装饰器跳过特定测试(如依赖未准备好、功能未实现):
class TestCalculatorDivide(unittest.TestCase):
@unittest.skip("除数为0的情况暂不测试") # 无条件跳过
def test_divide_by_zero(self):
...
@unittest.skipIf(sys.version_info < (3, 8), "仅支持Python 3.8+") # 条件跳过
def test_new_feature(self):
...
setUpClass()
/tearDownClass()
:在测试类的所有方法执行前后调用(仅执行一次);setUp()
/tearDown()
:在每个测试方法执行前后调用(执行多次)。示例(数据库测试场景):
class TestDatabase(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.db = connect_to_database() # 连接数据库(仅执行一次)
def setUp(self):
self.db.start_transaction() # 开始事务(每个测试方法前执行)
def test_query(self):
self.db.execute("SELECT * FROM users")
# 测试逻辑...
def tearDown(self):
self.db.rollback() # 回滚事务(每个测试方法后执行,避免脏数据)
@classmethod
def tearDownClass(cls):
cls.db.close() # 关闭数据库连接(仅执行一次)
unittest
是Python开发者必备的测试工具,掌握以下最佳实践可大幅提升测试效率:
test_divide_by_zero_should_raise_error
,失败时能快速定位问题;setUp/tearDown
隔离测试状态,避免测试间相互影响;python -m unittest discover
添加到CI/CD流程,每次提交代码自动运行测试;pytest
扩展unittest
(支持更灵活的断言和插件),用coverage
统计测试覆盖率。最后记住:好的单元测试不是负担,而是代码的“安全绳”。从今天开始,为你的每个函数、每个方法编写测试用例——它会在你修改代码时,用“测试通过”的绿色提示,告诉你“一切尽在掌握”!