文章目录
- 单元测试是对软件中最小可测试单元(如函数、方法、类)的独立验证,核心目标是: 验证代码行为符合预期(输入特定参数时,输出是否正确); 快速定位问题(修改代码后,若测试失败,可精准定位改动影响的范围); 支持安全重构(重构代码逻辑时,通过单元测试确保功能不退化); 提升代码质量(编写测试时,会倒逼开发者设计更易测试、低耦合的代码)。
- unittest 是 Python 标准库内置的单元测试框架,无需额外安装,核心特性包括: 提供 TestCase 基类,用于封装测试用例; 丰富的断言方法(如验证相等、包含、异常等); 支持用例组织(TestSuite)和批量执行; 提供 setUp()/tearDown() 等钩子方法,简化测试资源的初始化与清理; 支持生成测试报告(可结合第三方库优化格式)。
- 首先创建一个待测试的模块 calculator.py,包含加法、减法两个函数: # calculator.py def add(a, b): “””加法函数:返回 a + b 的结果””” return a + b def subtract(a, b): “””减法函数:返回 a – b 的结果””” return a – b
- 创建测试文件 test_calculator.py(测试文件建议以 test_ 开头,便于识别),继承 unittest.TestCase 编写测试类: # test_calculator.py import unittest from calculator import add, subtract # 导入待测试函数 # 测试类必须继承 unittest.TestCase class TestCalculator(unittest.TestCase): # 测试方法必须以 test_ 开头(unittest 会自动识别) def test_add(self): “””测试加法函数:正常场景、边界值、异常场景””” # 断言:验证 add(1, 2) 的结果是否等于 3 self.assertEqual(add(1, 2), 3) # 测试负数相加 self.assertEqual(add(-1, -2), -3) # 测试浮点数相加 self.assertAlmostEqual(add(0.1, 0.2), 0.3) # 浮点数精度问题用 assertAlmostEqual def test_subtract(self): “””测试减法函数””” self.assertEqual(subtract(5, 3), 2) self.assertEqual(subtract(3, 5), -2) self.assertEqual(subtract(0, 0), 0) # 运行测试(直接执行该文件时触发) if __name__ == ‘__main__’: unittest.main()
-
- 在终端执行 python test_calculator.py,输出如下: .. ———————————————————————- Ran 2 tests in 0.001s OK 输出解读: .. 表示 2 个测试用例都通过(每个 . 对应一个通过的测试方法); Ran 2 tests in 0.001s 表示执行了 2 个测试,耗时 0.001 秒; OK 表示所有测试用例通过。
- # 运行指定测试模块 python -m unittest test_calculator.py # 运行指定测试类 python -m unittest test_calculator.TestCalculator # 运行指定测试方法 python -m unittest test_calculator.TestCalculator.test_add
- python -m unittest test_calculator.py -v 输出如下(更清晰展示每个测试方法的执行结果): test_add (test_calculator.TestCalculator) 测试加法函数:正常场景、边界值、异常场景 … ok test_subtract (test_calculator.TestCalculator) 测试减法函数 … ok ———————————————————————- Ran 2 tests in 0.001s OK
- 断言方法 作用 示例 assertEqual(a, b, msg=None) 验证 a == b,msg 为自定义失败提示 self.assertEqual(add(2,3),5, "加法结果错误") assertNotEqual(a, b) 验证 a != b self.assertNotEqual(subtract(5,3), 3) assertAlmostEqual(a, b, places=7) 验证浮点数近似相等(places 为保留小数位) self.assertAlmostEqual(0.1+0.2, 0.3, places=1)
- 断言方法 作用 示例 assertTrue(x) 验证 x 为 True self.assertTrue(10 > 5) assertFalse(x) 验证 x 为 False self.assertFalse(10 < 5)
- 断言方法 作用 示例 assertIn(x, container) 验证 x 在 container 中 self.assertIn(3, [1,2,3]) assertNotIn(x, container) 验证 x 不在 container 中 self.assertNotIn(4, [1,2,3])
- 断言方法 作用 示例 assertRaises(exception, callable, *args, **kwargs) 验证调用函数时抛出指定异常 self.assertRaises(ZeroDivisionError, divide, 5, 0)
- 断言方法 作用 示例 assertIs(a, b) 验证 a is b(身份相等) self.assertIsNone(None) assertIsNone(x) 验证 x 是 None self.assertIsNone(get_user(999)) assertGreater(a, b) 验证 a > b self.assertGreater(10, 5)
- 如果多个测试方法需要使用相同的资源(如创建对象、连接数据库),可通过 setUp() 和 tearDown() 简化代码:
setUp():每个测试方法执行前自动调用(初始化资源);
tearDown():每个测试方法执行后自动调用(清理资源)。
- # test_calculator.py class TestCalculator(unittest.TestCase): # 每个测试方法执行前调用 def setUp(self): “””初始化测试资源:创建计算器实例(此处模拟,实际可用于数据库连接等)””” print(“===== 执行 setUp(),初始化资源 =====”) self.calc = { “add”: add, “subtract”: subtract, “divide”: divide } # 每个测试方法执行后调用 def tearDown(self): “””清理测试资源:此处无实际操作,仅作示例””” print(“===== 执行 tearDown(),清理资源 =====”) def test_add(self): self.assertEqual(self.calc[“add”](1,2), 3) def test_subtract(self): self.assertEqual(self.calc[“subtract”](5,3), 2) 执行后输出(可见 setUp 和 tearDown 被自动调用): ===== 执行 setUp(),初始化资源 ===== ===== 执行 tearDown(),清理资源 ===== .===== 执行 setUp(),初始化资源 ===== ===== 执行 tearDown(),清理资源 ===== . ———————————————————————- Ran 2 tests in 0.001s OK
- 如果资源只需初始化一次(如启动服务器、创建数据库连接池),可使用类级别的钩子方法(需配合 @classmethod 装饰器): class TestCalculator(unittest.TestCase): # 整个测试类执行前调用一次(类级别初始化) @classmethod def setUpClass(cls): print(“===== 执行 setUpClass(),初始化类级资源 =====”) cls.db_conn = “模拟数据库连接” # 示例:共享数据库连接 # 整个测试类执行后调用一次(类级别清理) @classmethod def tearDownClass(cls): print(“===== 执行 tearDownClass(),清理类级资源 =====”) cls.db_conn = None # 关闭连接
- 当需要批量执行指定的测试用例(而非全部)时,可通过 TestSuite 手动组合: # test_suite.py import unittest from test_calculator import TestCalculator def create_suite(): # 1. 创建测试套件 suite = unittest.TestSuite() # 2. 向套件中添加测试用例(多种方式) # 方式 1:添加单个测试方法 suite.addTest(TestCalculator(“test_add”)) suite.addTest(TestCalculator(“test_divide”)) # 方式 2:添加多个测试方法(列表形式) tests = [TestCalculator(“test_subtract”), TestCalculator(“test_divide”)] suite.addTests(tests) return suite if __name__ == ‘__main__’: # 3. 创建测试运行器并执行套件 runner = unittest.TextTestRunner(verbosity=2) # verbosity=2 显示详细日志 runner.run(create_suite()) 执行 python test_suite.py,会只运行套件中指定的测试方法。
- 如果项目中有多个测试文件(如 test_calculator.py、test_string.py),可通过 unittest.defaultTestLoader.discover() 自动发现并执行所有测试: # run_all_tests.py import unittest if __name__ == ‘__main__’: # 发现当前目录下所有以 test_ 开头的文件中的测试用例 suite = unittest.defaultTestLoader.discover( start_dir=’.’, # 搜索目录 pattern=’test_*.py’, # 测试文件匹配规则 top_level_dir=None # 顶级目录(默认 None 表示 start_dir) ) runner = unittest.TextTestRunner(verbosity=2) runner.run(suite)
- 实际开发中,可能需要跳过某些测试(如功能未完成、环境不支持),unittest 提供了 3 个常用装饰器:
装饰器
作用
示例
@unittest.skip(reason)
无条件跳过测试
@unittest.skip("功能未完成,暂不测试")
@unittest.skipIf(condition, reason)
条件为 True 时跳过
@unittest.skipIf(sys.version_info < (3.8), "Python3.8+ 才支持")
@unittest.skipUnless(condition, reason)
条件为 False 时跳过
@unittest.skipUnless(os.name == "posix", "仅 Linux/Mac 环境测试")
- import sys import unittest from calculator import add class TestSkipExample(unittest.TestCase): @unittest.skip(“临时跳过该测试”) def test_add_1(self): self.assertEqual(add(1,2), 3) @unittest.skipIf(sys.version_info < (3, 9), “Python3.9+ 才支持该特性”) def test_add_2(self): self.assertEqual(add(0.1, 0.2), 0.3) @unittest.skipUnless(sys.platform == “win32”, “仅 Windows 环境测试”) def test_add_3(self): self.assertEqual(add(-1, -1), -2) 执行后,跳过的测试会显示 s 标记: s.s ———————————————————————- Ran 3 tests in 0.000s OK (skipped=3)
- 如果某个测试用例已知会失败(如已知 bug 未修复),可使用 @unittest.expectedFailure 装饰器,标记为“预期失败”——执行失败时不会计入错误统计: class TestExpectedFailure(unittest.TestCase): @unittest.expectedFailure def test_divide_by_zero(self): # 已知该用例当前会失败(假设 bug 未修复) self.assertEqual(divide(5, 0), 0) # 实际会抛出异常,预期失败 执行后,该用例会显示 x 标记,不会导致测试整体失败。
- unittest 原生不支持参数化测试(即同一测试逻辑用多组参数重复执行),但可通过第三方库 ddt(Data-Driven Tests)实现。
- pip install ddt
- # test_parametrize.py import unittest from ddt import ddt, data, unpack # 导入 ddt 相关装饰器 from calculator import add # 1. 用 @ddt 装饰测试类 @ddt class TestAddParametrize(unittest.TestCase): # 2. 用 @data 传入多组测试数据(元组形式) @data( (1, 2, 3), # 输入 1+2,预期输出 3 (-1, -2, -3), # 输入 -1+-2,预期输出 -3 (0.1, 0.2, 0.3), # 浮点数测试 (100, 200, 300) # 大数测试 ) # 3. 用 @unpack 解包元组(让参数与测试方法参数一一对应) @unpack def test_add_param(self, a, b, expected): “””参数化测试加法函数””” self.assertAlmostEqual(add(a, b), expected) if __name__ == ‘__main__’: unittest.main() 执行后,会自动为每组数据生成一个测试用例,输出如下: …. ———————————————————————- Ran 4 tests in 0.001s OK
- unittest 默认生成文本报告,可读性较差。可通过第三方库 unittest-html-reporting 生成美观的 HTML 报告。
- pip install unittest-html-reporting
- # test_html_report.py import unittest from htmltestreport import HTMLTestReport # 注意导入方式(不同库可能不同) if __name__ == ‘__main__’: # 1. 发现所有测试用例 suite = unittest.defaultTestLoader.discover(start_dir=’.’, pattern=’test_*.py’) # 2. 生成 HTML 报告 with open(‘test_report.html’, ‘w’, encoding=’utf-8′) as f: runner = HTMLTestReport( stream=f, title=’计算器模块单元测试报告’, description=’测试加法、减法、除法函数的正确性’, tester=’开发者’ ) runner.run(suite) 执行后,会在当前目录生成 test_report.html 文件,打开后可看到测试通过率、用例详情、失败原因等信息,适合团队协作与汇报。
- 测试覆盖率用于统计“被测试代码的行数占总代码行数的比例”,帮助发现未被测试的代码。
- pip install coverage
- # 1. 执行测试并收集覆盖率数据(–source 指定待统计的模块) coverage run –source=calculator.py -m unittest test_calculator.py # 2. 查看文本格式的覆盖率报告 coverage report # 3. 生成 HTML 格式的覆盖率报告(更直观) coverage html
- Name Stmts Miss Cover ———————————— calculator.py 8 0 100% ———————————— TOTAL 8 0 100% Stmts:总代码行数; Miss:未被测试覆盖的行数; Cover:覆盖率(100% 表示所有代码都被测试覆盖)。 执行 coverage html 后,会生成 htmlcov 目录,打开 index.html 可查看详细的覆盖率报告(红色表示未覆盖,绿色表示已覆盖)。
- # user_manager.py class UserManager: def __init__(self): self.users = {} # 存储用户:key=用户ID,value=用户名 def add_user(self, user_id, username): “””新增用户:user_id 已存在则抛出 ValueError””” if not isinstance(user_id, int) or user_id <= 0: raise ValueError(“用户ID必须是正整数”) if user_id in self.users: raise ValueError(f”用户ID {user_id} 已存在”) self.users[user_id] = username return True def get_user(self, user_id): “””查询用户:返回用户名,不存在则返回 None””” return self.users.get(user_id) def delete_user(self, user_id): “””删除用户:返回是否删除成功””” if user_id not in self.users: return False del self.users[user_id] return True
- # test_user_manager.py import unittest from user_manager import UserManager class TestUserManager(unittest.TestCase): def setUp(self): “””每个测试用例前初始化:创建用户管理器实例””” self.manager = UserManager() def test_add_user_success(self): “””测试新增用户成功””” result = self.manager.add_user(1, “张三”) self.assertTrue(result) self.assertEqual(self.manager.get_user(1), “张三”) def test_add_user_duplicate_id(self): “””测试新增重复用户ID””” self.manager.add_user(1, “张三”) with self.assertRaises(ValueError) as ctx: self.manager.add_user(1, “李四”) self.assertEqual(str(ctx.exception), “用户ID 1 已存在”) def test_add_user_invalid_id(self): “””测试新增用户时传入无效ID(非整数、负数)””” # 非整数ID with self.assertRaises(ValueError) as ctx: self.manager.add_user(“a”, “张三”) self.assertEqual(str(ctx.exception), “用户ID必须是正整数”) # 负数ID with self.assertRaises(ValueError) as ctx: self.manager.add_user(-2, “李四”) self.assertEqual(str(ctx.exception), “用户ID必须是正整数”) def test_get_user_exist(self): “””测试查询存在的用户””” self.manager.add_user(2, “李四”) self.assertEqual(self.manager.get_user(2), “李四”) def test_get_user_not_exist(self): “””测试查询不存在的用户””” self.assertIsNone(self.manager.get_user(999)) def test_delete_user_success(self): “””测试删除存在的用户””” self.manager.add_user(3, “王五”) result = self.manager.delete_user(3) self.assertTrue(result) self.assertIsNone(self.manager.get_user(3)) def test_delete_user_not_exist(self): “””测试删除不存在的用户””” result = self.manager.delete_user(999) self.assertFalse(result) if __name__ == ‘__main__’: unittest.main(verbosity=2)
- python test_user_manager.py -v 输出如下(所有测试用例通过): test_add_user_invalid_id (test_user_manager.TestUserManager) 测试新增用户时传入无效ID(非整数、负数) … ok test_add_user_duplicate_id (test_user_manager.TestUserManager) 测试新增重复用户ID … ok test_add_user_success (test_user_manager.TestUserManager) 测试新增用户成功 … ok test_delete_user_not_exist (test_user_manager.TestUserManager) 测试删除不存在的用户 … ok test_delete_user_success (test_user_manager.TestUserManager) 测试删除存在的用户 … ok test_get_user_exist (test_user_manager.TestUserManager) 测试查询存在的用户 … ok test_get_user_not_exist (test_user_manager.TestUserManager) 测试查询不存在的用户 … ok ———————————————————————- Ran 7 tests in 0.001s OK
-
- 原因:测试方法未以 test_ 开头(unittest 只识别以 test_ 开头的方法); 解决:将方法名改为 test_xxx 格式(如 test_add 而非 add_test)。
- 现象:某个测试方法执行失败,因为它依赖前一个测试方法的执行结果; 原则:测试用例必须独立(互不依赖),每个用例应能单独运行; 解决:使用 setUp() 为每个用例重新初始化资源,避免依赖其他用例的执行结果。
- 现象:assertEqual(0.1+0.2, 0.3) 失败(浮点数精度问题); 解决:使用 assertAlmostEqual(a, b, places=1) 或 assertEqual(round(a+b, 1), 0.3)。
- 测试用例设计原则: 覆盖核心场景(正常输入、边界值、异常输入); 一个测试方法只验证一个核心逻辑(避免“大而全”的测试); 测试方法命名清晰(如 test_add_user_duplicate_id 而非 test_add_2)。 代码组织: 测试文件与源码文件分离(如 src/ 存源码,tests/ 存测试文件); 测试文件名与源码文件名对应(如 calculator.py → test_calculator.py)。 自动化集成: 将单元测试集成到 CI/CD 流程(如 GitHub Actions、Jenkins),每次提交代码自动执行测试; 要求测试覆盖率达到一定阈值(如 80%),避免未测试代码上线。 避免过度测试: 不测试第三方库或标准库的功能; 不测试实现细节(只测试接口行为,如函数输入输出,而非内部变量)。
目录
- 前言
- 一、什么是单元测试与 unittest?
- 1.1 单元测试的核心价值
- 1.2 unittest 模块简介
- 二、unittest 快速入门:编写第一个测试用例
- 2.1 步骤 1:编写待测试代码
- 2.2 步骤 2:编写 unittest 测试用例
- 2.3 步骤 3:执行测试与查看结果
- 方式 1:直接运行测试文件
- 方式 2:命令行指定测试模块/类/方法
- 方式 3:带详细日志执行(-v 参数)
- 三、unittest 核心语法:断言方法
- 3.1 equality 断言(验证相等/不等)
- 3.2 布尔值断言(验证 True/False)
- 3.3 包含关系断言
- 3.4 异常断言(验证函数抛出指定异常)
- 3.5 其他常用断言
- 代码示例:异常断言实战
- 四、用例组织:setUp/tearDown 与测试套件
- 4.1 setUp() 与 tearDown():用例级资源管理
- 代码示例:
- 4.2 setUpClass() 与 tearDownClass():类级资源管理
- 4.3 TestSuite:手动组织测试用例
- 4.4 自动发现测试用例
- 五、unittest 进阶用法
- 5.1 跳过测试用例(skip 装饰器)
- 代码示例:
- 5.2 预期失败(expectedFailure)
- 5.3 参数化测试(结合 ddt 库)
- 步骤 1:安装 ddt
- 步骤 2:参数化测试示例
- 5.4 生成 HTML 测试报告
- 步骤 1:安装库
- 步骤 2:生成 HTML 报告示例
- 5.5 测试覆盖率(结合 coverage 库)
- 步骤 1:安装 coverage
- 步骤 2:统计测试覆盖率
- 输出解读(文本报告):
- 六、实战案例:测试一个用户管理模块
- 6.1 待测试模块:user_manager.py
- 6.2 测试模块:test_user_manager.py
- 6.3 执行测试与查看结果
- 七、unittest 常见问题与最佳实践
- 7.1 常见问题
- 问题 1:测试方法未执行
- 问题 2:测试用例依赖顺序
- 问题 3:浮点数断言失败
- 7.2 最佳实践
- 八、unittest 与 pytest 对比
在软件开发中,“保证代码正确性”是核心需求之一。当我们编写函数、类或模块后,如何验证其行为符合预期?如何在修改代码后快速排查问题?单元测试是解决这些问题的关键技术,而 Python 内置的 unittest 模块(基于 xUnit 框架),正是实现单元测试的强大工具——无需额外安装,语法规范,支持自动化测试、用例组织、断言校验等核心功能。
单元测试是对软件中最小可测试单元(如函数、方法、类)的独立验证,核心目标是:
- 验证代码行为符合预期(输入特定参数时,输出是否正确);
- 快速定位问题(修改代码后,若测试失败,可精准定位改动影响的范围);
- 支持安全重构(重构代码逻辑时,通过单元测试确保功能不退化);
- 提升代码质量(编写测试时,会倒逼开发者设计更易测试、低耦合的代码)。
unittest 是 Python 标准库内置的单元测试框架,无需额外安装,核心特性包括:
- 提供
TestCase基类,用于封装测试用例; - 丰富的断言方法(如验证相等、包含、异常等);
- 支持用例组织(
TestSuite)和批量执行; - 提供
setUp()/tearDown()等钩子方法,简化测试资源的初始化与清理; - 支持生成测试报告(可结合第三方库优化格式)。
我们从一个简单场景入手:编写一个计算工具函数,并用 unittest 验证其正确性。
首先创建一个待测试的模块 calculator.py,包含加法、减法两个函数:
# calculator.py
def add(a, b):
"""加法函数:返回 a + b 的结果"""
return a + b
def subtract(a, b):
"""减法函数:返回 a - b 的结果"""
return a - b
创建测试文件 test_calculator.py(测试文件建议以 test_ 开头,便于识别),继承 unittest.TestCase 编写测试类:
# test_calculator.py
import unittest
from calculator import add, subtract # 导入待测试函数
# 测试类必须继承 unittest.TestCase
class TestCalculator(unittest.TestCase):
# 测试方法必须以 test_ 开头(unittest 会自动识别)
def test_add(self):
"""测试加法函数:正常场景、边界值、异常场景"""
# 断言:验证 add(1, 2) 的结果是否等于 3
self.assertEqual(add(1, 2), 3)
# 测试负数相加
self.assertEqual(add(-1, -2), -3)
# 测试浮点数相加
self.assertAlmostEqual(add(0.1, 0.2), 0.3) # 浮点数精度问题用 assertAlmostEqual
def test_subtract(self):
"""测试减法函数"""
self.assertEqual(subtract(5, 3), 2)
self.assertEqual(subtract(3, 5), -2)
self.assertEqual(subtract(0, 0), 0)
# 运行测试(直接执行该文件时触发)
if __name__ == '__main__':
unittest.main()
在终端执行 python test_calculator.py,输出如下:
.. ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
- 输出解读:
..表示 2 个测试用例都通过(每个.对应一个通过的测试方法);Ran 2 tests in 0.001s表示执行了 2 个测试,耗时 0.001 秒;OK表示所有测试用例通过。
# 运行指定测试模块
python -m unittest test_calculator.py
# 运行指定测试类
python -m unittest test_calculator.TestCalculator
# 运行指定测试方法
python -m unittest test_calculator.TestCalculator.test_add
python -m unittest test_calculator.py -v
输出如下(更清晰展示每个测试方法的执行结果):
test_add (test_calculator.TestCalculator) 测试加法函数:正常场景、边界值、异常场景 ... ok test_subtract (test_calculator.TestCalculator) 测试减法函数 ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
断言是单元测试的核心——通过断言判断代码实际输出是否与预期一致。unittest.TestCase 提供了丰富的断言方法,以下是最常用的类别:
| 断言方法 | 作用 | 示例 |
|---|---|---|
assertEqual(a, b, msg=None) |
验证 a == b,msg 为自定义失败提示 |
self.assertEqual(add(2,3),5, "加法结果错误") |
assertNotEqual(a, b) |
验证 a != b |
self.assertNotEqual(subtract(5,3), 3) |
assertAlmostEqual(a, b, places=7) |
验证浮点数近似相等(places 为保留小数位) | self.assertAlmostEqual(0.1+0.2, 0.3, places=1) |
| 断言方法 | 作用 | 示例 |
|---|---|---|
assertTrue(x) |
验证 x 为 True |
self.assertTrue(10 > 5) |
assertFalse(x) |
验证 x 为 False |
self.assertFalse(10 < 5) |
| 断言方法 | 作用 | 示例 |
|---|---|---|
assertIn(x, container) |
验证 x 在 container 中 |
self.assertIn(3, [1,2,3]) |
assertNotIn(x, container) |
验证 x 不在 container 中 |
self.assertNotIn(4, [1,2,3]) |
| 断言方法 | 作用 | 示例 |
|---|---|---|
assertRaises(exception, callable, *args, **kwargs) |
验证调用函数时抛出指定异常 | self.assertRaises(ZeroDivisionError, divide, 5, 0) |
| 断言方法 | 作用 | 示例 |
|---|---|---|
assertIs(a, b) |
验证 a is b(身份相等) |
self.assertIsNone(None) |
assertIsNone(x) |
验证 x 是 None |
self.assertIsNone(get_user(999)) |
assertGreater(a, b) |
验证 a > b |
self.assertGreater(10, 5) |
给 calculator.py 添加除法函数(包含除以 0 的异常场景):
# calculator.py
def divide(a, b):
"""除法函数:b 不能为 0,否则抛出 ZeroDivisionError"""
if b == 0:
raise ZeroDivisionError("除数不能为 0")
return a / b
在测试类中添加异常测试方法:
# test_calculator.py
def test_divide(self):
"""测试除法函数:正常场景 + 异常场景"""
# 正常除法
self.assertEqual(divide(10, 2), 5)
self.assertAlmostEqual(divide(3, 2), 1.5)
# 测试除以 0 抛出异常(两种写法)
# 写法 1:使用 assertRaises 作为上下文管理器
with self.assertRaises(ZeroDivisionError) as ctx:
divide(5, 0)
# 验证异常信息
self.assertEqual(str(ctx.exception), "除数不能为 0")
# 写法 2:直接传入函数和参数
self.assertRaises(ZeroDivisionError, divide, 5, 0)
当测试用例增多时,需要高效组织用例(如共享资源初始化、批量执行指定用例),unittest 提供了完善的支持。
如果多个测试方法需要使用相同的资源(如创建对象、连接数据库),可通过 setUp() 和 tearDown() 简化代码:
setUp():每个测试方法执行前自动调用(初始化资源);tearDown():每个测试方法执行后自动调用(清理资源)。
# test_calculator.py
class TestCalculator(unittest.TestCase):
# 每个测试方法执行前调用
def setUp(self):
"""初始化测试资源:创建计算器实例(此处模拟,实际可用于数据库连接等)"""
print("===== 执行 setUp(),初始化资源 =====")
self.calc = {
"add": add,
"subtract": subtract,
"divide": divide
}
# 每个测试方法执行后调用
def tearDown(self):
"""清理测试资源:此处无实际操作,仅作示例"""
print("===== 执行 tearDown(),清理资源 =====")
def test_add(self):
self.assertEqual(self.calc["add"](1,2), 3)
def test_subtract(self):
self.assertEqual(self.calc["subtract"](5,3), 2)
执行后输出(可见 setUp 和 tearDown 被自动调用):
===== 执行 setUp(),初始化资源 ===== ===== 执行 tearDown(),清理资源 ===== .===== 执行 setUp(),初始化资源 ===== ===== 执行 tearDown(),清理资源 ===== . ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
如果资源只需初始化一次(如启动服务器、创建数据库连接池),可使用类级别的钩子方法(需配合 @classmethod 装饰器):
class TestCalculator(unittest.TestCase):
# 整个测试类执行前调用一次(类级别初始化)
@classmethod
def setUpClass(cls):
print("===== 执行 setUpClass(),初始化类级资源 =====")
cls.db_conn = "模拟数据库连接" # 示例:共享数据库连接
# 整个测试类执行后调用一次(类级别清理)
@classmethod
def tearDownClass(cls):
print("===== 执行 tearDownClass(),清理类级资源 =====")
cls.db_conn = None # 关闭连接
当需要批量执行指定的测试用例(而非全部)时,可通过 TestSuite 手动组合:
# test_suite.py
import unittest
from test_calculator import TestCalculator
def create_suite():
# 1. 创建测试套件
suite = unittest.TestSuite()
# 2. 向套件中添加测试用例(多种方式)
# 方式 1:添加单个测试方法
suite.addTest(TestCalculator("test_add"))
suite.addTest(TestCalculator("test_divide"))
# 方式 2:添加多个测试方法(列表形式)
tests = [TestCalculator("test_subtract"), TestCalculator("test_divide")]
suite.addTests(tests)
return suite
if __name__ == '__main__':
# 3. 创建测试运行器并执行套件
runner = unittest.TextTestRunner(verbosity=2) # verbosity=2 显示详细日志
runner.run(create_suite())
执行 python test_suite.py,会只运行套件中指定的测试方法。
如果项目中有多个测试文件(如 test_calculator.py、test_string.py),可通过 unittest.defaultTestLoader.discover() 自动发现并执行所有测试:
# run_all_tests.py
import unittest
if __name__ == '__main__':
# 发现当前目录下所有以 test_ 开头的文件中的测试用例
suite = unittest.defaultTestLoader.discover(
start_dir='.', # 搜索目录
pattern='test_*.py', # 测试文件匹配规则
top_level_dir=None # 顶级目录(默认 None 表示 start_dir)
)
runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite)
实际开发中,可能需要跳过某些测试(如功能未完成、环境不支持),unittest 提供了 3 个常用装饰器:
| 装饰器 | 作用 | 示例 |
|---|---|---|
@unittest.skip(reason) |
无条件跳过测试 | @unittest.skip("功能未完成,暂不测试") |
@unittest.skipIf(condition, reason) |
条件为 True 时跳过 | @unittest.skipIf(sys.version_info < (3.8), "Python3.8+ 才支持") |
@unittest.skipUnless(condition, reason) |
条件为 False 时跳过 | @unittest.skipUnless(os.name == "posix", "仅 Linux/Mac 环境测试") |
import sys
import unittest
from calculator import add
class TestSkipExample(unittest.TestCase):
@unittest.skip("临时跳过该测试")
def test_add_1(self):
self.assertEqual(add(1,2), 3)
@unittest.skipIf(sys.version_info < (3, 9), "Python3.9+ 才支持该特性")
def test_add_2(self):
self.assertEqual(add(0.1, 0.2), 0.3)
@unittest.skipUnless(sys.platform == "win32", "仅 Windows 环境测试")
def test_add_3(self):
self.assertEqual(add(-1, -1), -2)
执行后,跳过的测试会显示 s 标记:
s.s ---------------------------------------------------------------------- Ran 3 tests in 0.000s OK (skipped=3)
如果某个测试用例已知会失败(如已知 bug 未修复),可使用 @unittest.expectedFailure 装饰器,标记为“预期失败”——执行失败时不会计入错误统计:
class TestExpectedFailure(unittest.TestCase):
@unittest.expectedFailure
def test_divide_by_zero(self):
# 已知该用例当前会失败(假设 bug 未修复)
self.assertEqual(divide(5, 0), 0) # 实际会抛出异常,预期失败
执行后,该用例会显示 x 标记,不会导致测试整体失败。
unittest 原生不支持参数化测试(即同一测试逻辑用多组参数重复执行),但可通过第三方库 ddt(Data-Driven Tests)实现。
pip install ddt
# test_parametrize.py
import unittest
from ddt import ddt, data, unpack # 导入 ddt 相关装饰器
from calculator import add
# 1. 用 @ddt 装饰测试类
@ddt
class TestAddParametrize(unittest.TestCase):
# 2. 用 @data 传入多组测试数据(元组形式)
@data(
(1, 2, 3), # 输入 1+2,预期输出 3
(-1, -2, -3), # 输入 -1+-2,预期输出 -3
(0.1, 0.2, 0.3), # 浮点数测试
(100, 200, 300) # 大数测试
)
# 3. 用 @unpack 解包元组(让参数与测试方法参数一一对应)
@unpack
def test_add_param(self, a, b, expected):
"""参数化测试加法函数"""
self.assertAlmostEqual(add(a, b), expected)
if __name__ == '__main__':
unittest.main()
执行后,会自动为每组数据生成一个测试用例,输出如下:
.... ---------------------------------------------------------------------- Ran 4 tests in 0.001s OK
unittest 默认生成文本报告,可读性较差。可通过第三方库 unittest-html-reporting 生成美观的 HTML 报告。
pip install unittest-html-reporting
# test_html_report.py
import unittest
from htmltestreport import HTMLTestReport # 注意导入方式(不同库可能不同)
if __name__ == '__main__':
# 1. 发现所有测试用例
suite = unittest.defaultTestLoader.discover(start_dir='.', pattern='test_*.py')
# 2. 生成 HTML 报告
with open('test_report.html', 'w', encoding='utf-8') as f:
runner = HTMLTestReport(
stream=f,
title='计算器模块单元测试报告',
description='测试加法、减法、除法函数的正确性',
tester='开发者'
)
runner.run(suite)
执行后,会在当前目录生成 test_report.html 文件,打开后可看到测试通过率、用例详情、失败原因等信息,适合团队协作与汇报。
测试覆盖率用于统计“被测试代码的行数占总代码行数的比例”,帮助发现未被测试的代码。
pip install coverage
# 1. 执行测试并收集覆盖率数据(--source 指定待统计的模块)
coverage run --source=calculator.py -m unittest test_calculator.py
# 2. 查看文本格式的覆盖率报告
coverage report
# 3. 生成 HTML 格式的覆盖率报告(更直观)
coverage html
Name Stmts Miss Cover
------------------------------------
calculator.py 8 0 100%
------------------------------------
TOTAL 8 0 100%
Stmts:总代码行数;
Miss:未被测试覆盖的行数;
Cover:覆盖率(100% 表示所有代码都被测试覆盖)。
Stmts:总代码行数;Miss:未被测试覆盖的行数;Cover:覆盖率(100% 表示所有代码都被测试覆盖)。执行 coverage html 后,会生成 htmlcov 目录,打开 index.html 可查看详细的覆盖率报告(红色表示未覆盖,绿色表示已覆盖)。
我们模拟一个简单的用户管理模块,包含“新增用户”“查询用户”“删除用户”功能,并用 unittest 设计完整的测试用例。
# user_manager.py
class UserManager:
def __init__(self):
self.users = {} # 存储用户:key=用户ID,value=用户名
def add_user(self, user_id, username):
"""新增用户:user_id 已存在则抛出 ValueError"""
if not isinstance(user_id, int) or user_id <= 0:
raise ValueError("用户ID必须是正整数")
if user_id in self.users:
raise ValueError(f"用户ID {user_id} 已存在")
self.users[user_id] = username
return True
def get_user(self, user_id):
"""查询用户:返回用户名,不存在则返回 None"""
return self.users.get(user_id)
def delete_user(self, user_id):
"""删除用户:返回是否删除成功"""
if user_id not in self.users:
return False
del self.users[user_id]
return True
# test_user_manager.py
import unittest
from user_manager import UserManager
class TestUserManager(unittest.TestCase):
def setUp(self):
"""每个测试用例前初始化:创建用户管理器实例"""
self.manager = UserManager()
def test_add_user_success(self):
"""测试新增用户成功"""
result = self.manager.add_user(1, "张三")
self.assertTrue(result)
self.assertEqual(self.manager.get_user(1), "张三")
def test_add_user_duplicate_id(self):
"""测试新增重复用户ID"""
self.manager.add_user(1, "张三")
with self.assertRaises(ValueError) as ctx:
self.manager.add_user(1, "李四")
self.assertEqual(str(ctx.exception), "用户ID 1 已存在")
def test_add_user_invalid_id(self):
"""测试新增用户时传入无效ID(非整数、负数)"""
# 非整数ID
with self.assertRaises(ValueError) as ctx:
self.manager.add_user("a", "张三")
self.assertEqual(str(ctx.exception), "用户ID必须是正整数")
# 负数ID
with self.assertRaises(ValueError) as ctx:
self.manager.add_user(-2, "李四")
self.assertEqual(str(ctx.exception), "用户ID必须是正整数")
def test_get_user_exist(self):
"""测试查询存在的用户"""
self.manager.add_user(2, "李四")
self.assertEqual(self.manager.get_user(2), "李四")
def test_get_user_not_exist(self):
"""测试查询不存在的用户"""
self.assertIsNone(self.manager.get_user(999))
def test_delete_user_success(self):
"""测试删除存在的用户"""
self.manager.add_user(3, "王五")
result = self.manager.delete_user(3)
self.assertTrue(result)
self.assertIsNone(self.manager.get_user(3))
def test_delete_user_not_exist(self):
"""测试删除不存在的用户"""
result = self.manager.delete_user(999)
self.assertFalse(result)
if __name__ == '__main__':
unittest.main(verbosity=2)
python test_user_manager.py -v
输出如下(所有测试用例通过):
test_add_user_invalid_id (test_user_manager.TestUserManager) 测试新增用户时传入无效ID(非整数、负数) ... ok test_add_user_duplicate_id (test_user_manager.TestUserManager) 测试新增重复用户ID ... ok test_add_user_success (test_user_manager.TestUserManager) 测试新增用户成功 ... ok test_delete_user_not_exist (test_user_manager.TestUserManager) 测试删除不存在的用户 ... ok test_delete_user_success (test_user_manager.TestUserManager) 测试删除存在的用户 ... ok test_get_user_exist (test_user_manager.TestUserManager) 测试查询存在的用户 ... ok test_get_user_not_exist (test_user_manager.TestUserManager) 测试查询不存在的用户 ... ok ---------------------------------------------------------------------- Ran 7 tests in 0.001s OK
- 原因:测试方法未以
test_ 开头(unittest 只识别以 test_ 开头的方法);
- 解决:将方法名改为
test_xxx 格式(如 test_add 而非 add_test)。
test_ 开头(unittest 只识别以 test_ 开头的方法);test_xxx 格式(如 test_add 而非 add_test)。
- 现象:某个测试方法执行失败,因为它依赖前一个测试方法的执行结果;
- 原则:测试用例必须独立(互不依赖),每个用例应能单独运行;
- 解决:使用
setUp() 为每个用例重新初始化资源,避免依赖其他用例的执行结果。
setUp() 为每个用例重新初始化资源,避免依赖其他用例的执行结果。
- 现象:
assertEqual(0.1+0.2, 0.3) 失败(浮点数精度问题);
- 解决:使用
assertAlmostEqual(a, b, places=1) 或 assertEqual(round(a+b, 1), 0.3)。
assertEqual(0.1+0.2, 0.3) 失败(浮点数精度问题);assertAlmostEqual(a, b, places=1) 或 assertEqual(round(a+b, 1), 0.3)。
-
测试用例设计原则:
- 覆盖核心场景(正常输入、边界值、异常输入);
- 一个测试方法只验证一个核心逻辑(避免“大而全”的测试);
- 测试方法命名清晰(如
test_add_user_duplicate_id 而非 test_add_2)。
-
代码组织:
- 测试文件与源码文件分离(如
src/ 存源码,tests/ 存测试文件);
- 测试文件名与源码文件名对应(如
calculator.py → test_calculator.py)。
-
自动化集成:
- 将单元测试集成到 CI/CD 流程(如 GitHub Actions、Jenkins),每次提交代码自动执行测试;
- 要求测试覆盖率达到一定阈值(如 80%),避免未测试代码上线。
-
避免过度测试:
- 不测试第三方库或标准库的功能;
- 不测试实现细节(只测试接口行为,如函数输入输出,而非内部变量)。
测试用例设计原则:
- 覆盖核心场景(正常输入、边界值、异常输入);
- 一个测试方法只验证一个核心逻辑(避免“大而全”的测试);
- 测试方法命名清晰(如
test_add_user_duplicate_id而非test_add_2)。
代码组织:
- 测试文件与源码文件分离(如
src/存源码,tests/存测试文件); - 测试文件名与源码文件名对应(如
calculator.py→test_calculator.py)。
自动化集成:
- 将单元测试集成到 CI/CD 流程(如 GitHub Actions、Jenkins),每次提交代码自动执行测试;
- 要求测试覆盖率达到一定阈值(如 80%),避免未测试代码上线。
避免过度测试:
- 不测试第三方库或标准库的功能;
- 不测试实现细节(只测试接口行为,如函数输入输出,而非内部变量)。
unittest 是 Python 内置框架,稳定可靠,但语法较繁琐(需继承类、方法名固定);而 pytest 是第三方框架,语法更简洁(无需继承类、支持函数式测试),且兼容 unittest 用例。
| 特性 | unittest | pytest |
|---|---|---|
| 安装 | 内置,无需安装 | 需 pip install pytest |
| 语法 | 必须继承 TestCase,方法名以 test_ 开头 |
支持函数式测试、类测试,更灵活 |
| 断言 | 只能用 self.assertEqual() 等方法 |
支持原生 == 断言,也兼容 unittest 断言 |
| 参数化 | 需第三方库(ddt) | 内置 @pytest.mark.parametrize |
| 插件生态 | 较少 | 丰富(如 pytest-html、pytest-cov) |
如果是新手入门或开发标准库项目,unittest 足够使用;如果追求更高效率和灵活性,可尝试 pytest(兼容已有 unittest 用例,迁移成本低)。
若需进一步深入,可参考 Python 官方文档:unittest — 单元测试框架。
到此这篇关于Python unittest快速入门使用指南的文章就介绍到这了,更多相关Python unittest使用内容请搜索风君子博客以前的文章或继续浏览下面的相关文章希望大家以后多多支持风君子博客!
您可能感兴趣的文章:
- python单元测试unittest实例详解
- Python单元测试框架unittest使用方法讲解
- Python中unittest用法实例
- Python Unittest自动化单元测试框架详解
- Python unittest 简单实现参数化的方法
- Python中Unittest框架的具体使用
- Python unittest单元测试框架的使用
- Python Unittest根据不同测试环境跳过用例的方法
- python unittest实现api自动化测试