ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Python编写单元测试代码实例

Python编写单元测试代码实例 前言unittest是 Python 标准库自带的测试框架不需要安装任何第三方包。它的设计借鉴了 Java 的 JUnit所以你会看到TestCase、setUp、tearDown这类命名。虽然现在很多人转向 pytest但unittest依然值得学一是零依赖二是大量存量项目用它三是理解它的结构后看别的测试框架会轻松很多。这篇是实操向从写第一个TestCase开始讲到assertEqual系列断言、setUp/tearDown的生命周期、assertRaises怎么验证异常、unittest.mock怎么打桩最后说清测试目录该怎么组织、怎么用一条命令跑起来。文中所有 API 名称与参数以 Python 官方文档unittest与unittest.mock页面为准。示例以 Python 3 为基准。一、第一个 TestCase核心结构是继承unittest.TestCase把每个测试写成一个以test_开头的方法。# 适用于 Python 3.8import unittestdef add(a, b):return a bdef divide(a, b):if b 0:raise ValueError(除数不能为 0)return a / bclass TestCalculator(unittest.TestCase):def test_add(self):self.assertEqual(add(2, 3), 5)def test_divide(self):self.assertEqual(divide(10, 2), 5)def test_divide_by_zero(self):with self.assertRaises(ValueError):divide(1, 0)if __name__ __main__:unittest.main()几点解释方法名必须以test开头否则测试加载器不会收集它。这是最常见的「写了测试却一个都没跑」的原因。unittest.main()会自动发现本模块里的测试并运行方便单文件直接python xxx.py执行。断言失败时unittest会打印期望值与实际值并指出是哪个方法、哪一行。二、常用断言方法TestCase提供了一整套以assert开头的方法。不要用裸assert语句因为assert在-O优化模式下会被整体移除测试就形同虚设。用断言方法还能得到更好的失败信息。方法用途备注assertEqual(a, b)判断相等最常用底层用assertNotEqual(a, b)判断不等assertTrue(x)/assertFalse(x)判断真假assertIs(a, b)/assertIsNot(a, b)判断同一对象比assertEqual严格assertIsNone(x)判断是NoneassertIn(a, b)/assertNotIn(a, b)成员判断assertRaises(exc)断言抛异常上下文管理器用法assertAlmostEqual(a, b)浮点近似相等避免浮点直接比较assertCountEqual(a, b)两序列元素相同不计顺序一个补充说明旧的assertEquals、assertNotEquals是assertEqual、assertNotEqual的别名官方已把它们标记为废弃deprecated新代码应当用不带s的版本。浮点数比较要特别注意0.1 0.2 0.3在二进制浮点下不成立所以应该用assertAlmostEqual# 适用于 Python 3.8import unittestclass TestFloat(unittest.TestCase):def test_float_sum(self):self.assertAlmostEqual(0.1 0.2, 0.3, places7)def test_raises_with_message(self):with self.assertRaises(KeyError) as ctx:{}[missing]self.assertIn(missing, str(ctx.exception))三、setUp 与 tearDown测试夹具测试之间应当相互独立一个测试的状态不能泄漏到下一个。setUp()在每个测试方法之前运行tearDown()在每个测试方法之后运行——用它们准备和清理夹具fixture。# 适用于 Python 3.8import unittestclass Calculator:def __init__(self):self.history []def add(self, a, b):result a bself.history.append(result)return resultclass TestCalculator(unittest.TestCase):def setUp(self):# 每个测试都拿到一个全新的实例互不污染self.calc Calculator()def tearDown(self):# 每个测试结束后清理这里没有外部资源仅演示位置self.calc Nonedef test_add_records_history(self):self.calc.add(1, 2)self.assertEqual(self.calc.history, [3])def test_history_starts_empty(self):# 上一个测试加过的结果不会出现在这里self.assertEqual(self.calc.history, [])生命周期规则要记准setUp()抛异常该测试方法不会执行tearDown()也不会执行如果setUp()成功则无论测试方法是否失败tearDown()都会执行更粗粒度还有setUpClass()/tearDownClass()用classmethod装饰整个类跑一次和setUpModule()/tearDownModule()整个模块跑一次适合建立昂贵的、所有测试共享的资源比如临时数据库。四、用 mock 隔离外部依赖单元测试只应测「被测单元」不该真的去连数据库、发 HTTP 请求。unittest.mock用来把这些外部依赖替换成可控的假对象。最常用的是patch它既可以当装饰器也可以当上下文管理器。# 适用于 Python 3.8# 被测模块 weather.py 里写# import requests# def get_temp(city):# resp requests.get(fhttps://api.example.com/{city})# return resp.json()[temp]import unittestfrom unittest.mock import patch, MagicMockimport weather # 上面那个被测模块class TestGetTemp(unittest.TestCase):patch(weather.requests) # patch 被测模块里的名字不是全局 requestsdef test_get_temp(self, mock_requests):mock_requests.get.return_value.json.return_value {temp: 21}self.assertEqual(weather.get_temp(shanghai), 21)mock_requests.get.assert_called_once_with(https://api.example.com/shanghai)def test_with_magicmock_argument(self):# 另一种做法把假的 client 当参数注入不碰请求库client MagicMock()client.get.return_value.json.return_value {name: Ada}resp client.get(/users/1)self.assertEqual(resp.json()[name], Ada)关键要点patch 的位置要 patch「使用它的地方」而不是「定义它的地方」。上面被测模块weather.py里import requests后调用requests.get就应该 patchweather.requests这个名字而不是全局的requests。MagicMock会自动生成属性和方法所以client.get().json()这种链式调用不需要手工打桩。assert_called_once_with(...)、assert_called_with(...)用来验证调用参数是「行为测试」的常用手段。想模拟异常给 mock 设side_effect。五、测试目录怎么组织怎么跑起来推荐把源码和测试分开放测试文件名以test_开头这样unittest的自动发现能识别project/├── myservice/│ ├── __init__.py│ └── calculator.py└── tests/├── __init__.py├── test_calculator.py└── test_api.py运行方式有三种# 1. 跑单个文件python -m unittest tests.test_calculator# 2. 自动发现当前目录下所有 test*.pypython -m unittest discover# 3. 指定起始目录与匹配模式python -m unittest discover -s tests -p test_*.py -v注意discover默认匹配的是test*.pytest开头不是*_test.py。如果你的文件命名成calculator_test.py默认发现不了——要么改文件名要么用-p指定模式。另外测试目录里放一个__init__.py能让「按包名运行」在多数配置下更稳妥。常见坑点测试方法不以test开头❌def check_add(self): ...✅def test_add(self): ...用裸 assert 而不是断言方法❌assert add(1, 1) 2-O下会被优化掉✅self.assertEqual(add(1, 1), 2)测试之间共享可变状态❌ 在类属性里建一个列表多个测试往里塞数据✅ 在setUp()里每个测试新建独立实例浮点数直接assertEqual❌self.assertEqual(0.1 0.2, 0.3)不相等✅self.assertAlmostEqual(0.1 0.2, 0.3)patch 错了目标❌ patch 库定义处结果被测模块用了自己的引用没被替换✅ patch 被测模块里「引用它的那个名字」测试里做真实的网络 / 数据库访问❌ 测试依赖外部服务是否在线慢且不稳✅ 用mock隔离外部依赖文件名不匹配发现规则❌calculator_test.py用默认discover跑不到✅ 改成test_calculator.py或用-p指定模式依赖tearDown在setUp失败时执行❌setUp抛异常还想靠tearDown清理✅ 记住setUp失败时tearDown不会执行清理逻辑要能容忍总结概念作用关键约束TestCase测试容器方法名以test开头setUp/tearDown每个测试前后setUp失败则tearDown不跑setUpClass/tearDownClass每类一次需classmethod断言方法验证结果别用裸assertmock.patch隔离依赖patch 使用处而非定义处discover批量运行默认匹配test*.pyunittest的骨架说穿了就四块继承TestCase、test_命名、setUp准备、断言验证。把这四块用熟练再学 pytest 只是换个更顺手的皮。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表