尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Python unittest单元测试入门到实战:掌握断言、Mock与回归保护

Python unittest单元测试入门到实战:掌握断言、Mock与回归保护 写代码的人不管你是刚入职场的毕业生还是已经在业务线上摸爬滚打几年的老开发大概率都经历过这种场景功能自测时用print大法打印一堆中间变量肉眼盯着输出看半天改了一个工具函数心里没底怕把别的模块带崩线上出了bug排查半天发现是最早写的那批老代码出的问题可当时根本没留下任何测试痕迹。这些痛点归根到底就是一件事——缺少一套规范的、能自动化运行的回归保护网。而Python标准库里自带的单元测试框架unittest就是这张网最基础的形态。很多人觉得unittest名字老气、写法啰嗦不如pytest简洁但你别忘了它是Python安装完毕后开箱即用的官方测试框架零依赖、跨平台、文档齐全团队协作时不需要额外约定环境。这篇内容我会用一个实际的订单折扣计算场景把unittest从基础用例编写、断言方法、Fixture生命周期管理到Mock外部依赖、参数化测试、覆盖率和常见坑位全部过一遍适合刚接触单元测试的初学者也适合想系统梳理unittest用法的开发者参考。1. unittest是什么以及为什么值得用1.1 先理解单元测试框架的工作逻辑单元测试按字面意思理解就是针对代码里最小的可测试单元——通常是一个函数或一个类的方法——进行验证。但这里说的“验证”不是你把代码跑一遍看结果对不对而是“把输入、预期输出、执行条件全部写进代码由框架自动运行并对比实际输出是否匹配预期”的过程。unittest作为Python官方提供的单元测试框架做的事情本质上就是一个“测试调度员”它负责收集你写的测试用例按照规则执行它们记录哪些通过、哪些失败、失败原因是什么最后给你一份清晰的报告。把它类比成考试阅卷系统可能更好理解。你的被测函数是考生测试用例是标准答案和评分标准unittest则是那个自动阅卷系统。你不需要每次修改代码后都手动打开一个交互式环境把各种参数试一遍而是写一次测试代码以后每次改动代码后跑一下测试命令系统自动告诉你哪些地方被改挂了。这个“可重复、可回归、可报告”的机制才是单元测试框架区别于“手写脚本做自测”的根本。1.2 为什么Python官方选择unittest作为默认测试框架Python生态里现在测试框架很多pytest、nose、testtools等等每个都有自己的拥趸。那为什么还要死在标准库里保留unittest我觉得有几个核心原因值得理解。第一零依赖。unittest是标准库的一部分Python安装完就有不依赖pip安装任何第三方包。这在离线环境、内网部署、或者一些严格的依赖管控项目里特别重要。你不能要求生产环境里的每一台机器都联网装pytest但unittest一定能跑。第二它是经典的xUnit风格。unittest是从Java的JUnit移植过来的设计思路如果你熟悉JUnit、NUnit、Go的testing包几乎不需要重新学习就能上手。这种“TestCase类 断言方法 Fixture生命周期”的模式在全世界范围内经过了几十年的实战验证设计上非常稳健。它虽然代码上显得啰嗦但结构清晰团队协作时每个人写出来的测试风格都是统一的这对后期维护是极大的红利。第三它沉淀时间长、官方文档完善。Python官方文档里对unittest的说明非常详细而且标准库的API稳定性极强。基本不用担心升个Python版本测试框架接口就变了的情况。对长期维护的项目来说这种稳定性比“写法简洁”更有价值。我个人的观点是pytest确实好用它在unittest的基础上做了大量的语法糖和插件扩展但如果你连unittest都还没吃透直接跳到pytest其实会错过很多测试设计的核心思想。unittest的“啰嗦”恰恰逼着你想清楚每个测试组件的职责边界。把这个基础打牢后面学什么都快。2. 从零搭建unittest的基本用法2.1 环境准备与第一个测试用例先说结论不需要任何额外安装Python 3.x环境里直接能用。验证方式很简单打开终端输入python -m unittest --help能看到帮助信息就说明环境OK。下面我以一个实际项目片段为例一步步带你搭一个基于unittest的测试文件。假设现在有一个电商订单模块里面有个计算订单总价的函数正常逻辑是这样# order.py def calculate_total_price(items, coupon_codeNone, is_memberFalse): 计算订单总价 :param items: list of dict每个元素包含 price 和 quantity 字段 :param coupon_code: 优惠码SALE2024 表示整单8折 :param is_member: 是否会员会员在优惠码基础上再打9折 :return: 订单总价保留两位小数 total 0.0 for item in items: total item[price] * item[quantity] if coupon_code SALE2024: total * 0.8 if is_member: total * 0.9 return round(total, 2)这个函数很简单但刚好能引出单元测试要关注的核心问题正常输入、异常输入、边界情况、不同条件分支的组合。接下来在同一个目录下新建一个test_order.py文件import unittest from order import calculate_total_price class TestCalculateTotalPrice(unittest.TestCase): def test_normal_order(self): 普通订单两件商品无优惠非会员 items [ {price: 100, quantity: 2}, {price: 50, quantity: 1}, ] self.assertEqual(calculate_total_price(items), 250.0) def test_coupon_code_discount(self): 使用优惠码 SALE2024订单打8折 items [{price: 100, quantity: 1}] self.assertEqual(calculate_total_price(items, coupon_codeSALE2024), 80.0) def test_member_discount(self): 会员在无优惠码时打9折 items [{price: 100, quantity: 1}] self.assertEqual(calculate_total_price(items, is_memberTrue), 90.0) if __name__ __main__: unittest.main()在你的终端里执行python -m unittest test_order或者直接python test_order.py你会看到类似这样的输出... ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK三个测试全部通过。当你把calculate_total_price里的total * 0.8改成total * 0.75再跑一遍测试马上就能看到FAILED (failures1)并且会明确告诉你哪个用例、哪一行断言失败了。这就是自动化回归测试给你兜底的第一层价值任何改动只要破坏了原有行为测试会第一时间响警报。2.2 断言方法的选择与使用测试用例的核心动作是“断言”——把被测函数的实际输出和预期值做比较。unittest中最常用的断言方法就那几个但使用不当会踩一些很隐蔽的坑。下面这个表整理了实际开发中最高频的断言方法及其典型用途断言方法用途典型场景assertEqual(a, b)判断a与b相等数值、字符串比较assertNotEqual(a, b)判断a与b不相等确认某个参数确实改变了assertTrue(x)/assertFalse(x)判断布尔值验证开关类逻辑、条件判断assertIsNone(x)/assertIsNotNone(x)判断是否为None验证函数有/无返回值assertIn(a, b)判断a是否在b中验证某个元素在列表/字典键中assertAlmostEqual(a, b, places2)判断浮点数近似相等避免浮点精度导致误报assertRaises(SomeException, func, *args)判断是否抛指定异常参数校验、非法输入测试assertIsInstance(obj, cls)判断对象类型验证工厂函数返回类型assertDictEqual(a, b)判断两个字典相等接口返回dict对比这里我要特别强调一个实际踩过无数次的坑浮点数比较永远不要用assertEqual。比如上面计算总价的例子如果函数最后没有round(total, 2)那么calculate_total_price可能返回80.00000000000001而不是80.0此时assertEqual(80.0, 80.00000000000001)会失败但你的业务逻辑本身没任何问题。正确的做法是用assertAlmostEqual它会在指定精度范围内比较浮点数避免这种“因为计算机二进制存储精度而误报”的烦恼。另一个要注意的是异常断言。assertRaises除了上面表格里的写法还支持上下文管理器写法当你需要验证异常信息中包含特定内容时后者更灵活def test_invalid_items_raises(self): items [{price: -1, quantity: 1}] # 负数价格假设业务上不允许 with self.assertRaises(ValueError) as ctx: calculate_total_price(items) self.assertIn(price must be non-negative, str(ctx.exception))这样既能验证抛了异常还能验证异常信息是否符合预期比单纯判断“它抛异常了”更有价值。2.3 TestCase之间的组织suite与loader当测试文件数量多起来之后你不可能每次都手动指定python -m unittest test_order。unittest提供了一套测试收集机制自动发现并执行所有符合规则的测试。默认情况下unittest的测试发现规则是这样的从当前目录或你指定的目录递归查找所有test*.py文件匹配test*.py模式然后从这些文件里收集所有继承TestCase的类中以test开头的方法作为测试用例统一执行。也就是说你只要把文件名按test_xxx.py命名类名继承TestCase方法名以test开头然后在项目根目录执行python -m unittest discover -s ./tests -p test_*.py它就会自动把tests目录下所有符合模式的文件里所有测试用例全部收集起来跑一遍。这里-s指定起始目录-p指定文件匹配模式。但如果你的测试执行顺序有要求比如某几个用例必须先跑完才能跑后面的那就要用TestSuite手动组织import unittest from test_order import TestCalculateTotalPrice from test_user import TestUserLogin def suite(): test_cases [ TestCalculateTotalPrice, TestUserLogin, ] s unittest.TestSuite() loader unittest.TestLoader() for case in test_cases: tests loader.loadTestsFromTestCase(case) s.addTests(tests) return s if __name__ __main__: runner unittest.TextTestRunner(verbosity2) runner.run(suite())注意手动构建suite时用例的执行顺序是TestLoader内部决定的对类内的测试方法按名称排序执行按ASCII码顺序不是按照你方法定义的先后顺序。所以如果你的用例之间有依赖关系不要指望类的定义顺序就是执行顺序必须显式地用addTest或addTests明确安排。不过说实话单元测试的用例之间应该尽可能相互独立、互不依赖。如果出现“必须先跑A再跑B才能通过”的情况通常说明测试设计出了问题或者被测代码存在状态残留后文我会专门展开讲这个问题。2.4 命令行运行与IDE运行开发阶段我推荐在IDE里直接逐个运行测试方便调试但集成到CI/CD流程或者在服务器上做回归测试时命令行才是主流形态。unittest的命令行用法其实很简单但很多人会混淆几个参数的区别。python -m unittest test_module是运行指定模块python -m unittest discover是自动发现并运行所有测试python -m unittest -v是输出详细日志会打印每个用例的名称和结果。关于verbosity级别0表示只显示最终结果1表示每个用例输出一个点成功或F失败2则会显示用例名。我一般调试阶段用-v正式CI里用1级别就够了输出精简。另外一个实用技巧是运行单个测试用例类或单个测试方法# 运行某个测试类 python -m unittest test_order.TestCalculateTotalPrice # 运行某个测试方法 python -m unittest test_order.TestCalculateTotalPrice.test_member_discount这种指定粒度运行的方式在排查失败用例时特别高效不用每轮都跑全量测试。3. 测试夹具Fixture与生命周期管理3.1 setUp和tearDown到底什么时候执行测试夹具Fixture这个词听着高深其实指的就是“跑测试前后需要准备和清理的环境”。在unittest中最常见的夹具就是setUp和tearDown两个方法。setUp在每个测试用例执行之前运行tearDown在每个测试用例执行之后运行。注意是“每个”不是“全部”。假设一个测试类里有10个用例那么setUp和tearDown各会执行10次每次用例执行前先跑setUp用例结束后跑tearDown。这样的好处是每个用例都处在一个全新的、被重新初始化的环境里不会受到上个用例残留数据的影响。来看一个实际例子。假如你的订单模块需要连接数据库把订单数据存到一张表里import unittest import sqlite3 class TestOrderDatabase(unittest.TestCase): def setUp(self): # 每个测试前创建内存数据库和表 self.conn sqlite3.connect(:memory:) self.cursor self.conn.cursor() self.cursor.execute( CREATE TABLE orders ( id INTEGER PRIMARY KEY, item_name TEXT, amount REAL ) ) self.conn.commit() def tearDown(self): # 每个测试后关闭连接 self.cursor.close() self.conn.close() def test_insert_order(self): self.cursor.execute( INSERT INTO orders (item_name, amount) VALUES (?, ?), (keyboard, 299.0), ) self.conn.commit() self.cursor.execute(SELECT COUNT(*) FROM orders) count self.cursor.fetchone()[0] self.assertEqual(count, 1) def test_no_order_in_empty_table(self): self.cursor.execute(SELECT COUNT(*) FROM orders) count self.cursor.fetchone()[0] self.assertEqual(count, 0)这个例子中test_no_order_in_empty_table能通过的唯一原因就是setUp在它执行前重建了一张空表清理掉了test_insert_order写入的数据。如果你把建表逻辑放到类级别只执行一次第二个用例大概率会失败并报“订单数量为1而不是0”。3.2 setUpClass和tearDownClass什么时候真正需要它们有时候你会觉得每一条用例都重新准备完整环境太浪费特别是数据库连接、网络连接、大型配置文件的加载这类耗时操作。这时可以用setUpClass和tearDownClass它们在类中所有用例执行前只执行一次在所有用例执行后执行一次。使用方式有两点要注意一是必须加classmethod装饰器二是参数是cls而不是selfimport unittest class TestHeavyResource(unittest.TestCase): classmethod def setUpClass(cls): # 整个测试类只执行一次 cls.db_connection create_expensive_connection() classmethod def tearDownClass(cls): # 整个测试类结束后执行一次 cls.db_connection.close() def test_use_connection(self): self.assertIsNotNone(self.db_connection)但我必须提醒一句用setUpClass时需要格外谨慎因为它引入的是“类内共享状态”。一旦某个用例修改了共享状态其他用例看到的就是被改过的数据这很容易导致用例之间的隐式依赖和顺序敏感。我的建议是只有当环境准备成本真的高到难以接受时才考虑用setUpClass另外任何被setUpClass初始化的资源都应该设计成“只读”或者“可恢复”的状态否则就别省这个开销。还有一个更强的工具是addClassCleanup它允许你把清理操作注册到类级别即使部分用例失败清理函数也保证执行。这在异常处理流程里非常有用可以有效防止某个用例挂掉后把连接资源泄漏给后续用例。3.3 跳过测试、预期失败与条件执行实际项目中测试代码经常会碰到“当前环境跑不了”的情况。比如某些用例依赖Windows平台的API在Linux上根本没法跑或者某个功能还没开发完但你已经写了它的测试希望它先被跳过而不是失败。unittest为此提供了三个实用工具import unittest import sys class TestConditionalSkip(unittest.TestCase): unittest.skip(功能尚未实现暂不运行) def test_not_implemented_yet(self): # 该用例会被跳过 pass unittest.skipIf(sys.platform.startswith(win), 该特性不支持Windows平台) def test_windows_only_feature(self): pass unittest.skipUnless(hasattr(dict, merge), 需要Python 3.9的新特性) def test_dict_merge(self): pass unittest.expectedFailure def test_known_bug(self): # 这个用例已知会失败但为了保留记录标记为expectedFailure # 如果它竟然通过了会在报告里显示 UNEXPECTED SUCCESS self.assertEqual(1, 2)skip是无条件跳过skipIf是满足条件就跳过skipUnless是不满足条件就跳过相当于if notexpectedFailure则标记一个“预期会失败”的用例非常适合用来记录已知问题。跳过和预期失败的用例在报告里会显示为“s”“x”等状态不会拖垮整个测试套件的通过率同时又保留了未来修复后回归验证的入口。我曾经在一个老项目里用skipIf配合环境变量实现了“本地开发时跳过慢速集成测试CI时全量执行”的效果import os import unittest unittest.skipIf(os.environ.get(RUN_SLOW_TESTS) ! 1, 设置环境变量 RUN_SLOW_TESTS1 来运行慢测试) class TestSlowIntegration(unittest.TestCase): ...这样日常开发跑测试飞快CI流程里按需放开限制非常灵活。4. 复杂场景实战Mock、参数化与测试数据4.1 Mock对象把外部依赖关在门外单元测试的核心要求是“只测当前单元本身不依赖外部环境”。这句话说起来轻巧做起来很难。你测订单模块时它内部可能调用了支付接口测用户注册时它可能发了短信验证码。真实环境下这些外部依赖要么不稳定、要么有副作用、要么干脆在测试环境不存在。这时候就需要Mock。unittest.mock是标准库提供的mock模块核心能力是“用一个假对象替换真对象并记录它的调用情况”。它最经典的应用场景是模拟外部API返回from unittest import mock import unittest import my_payment # 假设这里有支付接口的封装 def pay_for_order(order_id, amount): # 业务代码内部会调用 my_payment.charge resp my_payment.charge(order_idorder_id, amountamount) return resp[status] success class TestPayForOrder(unittest.TestCase): mock.patch(my_payment.charge) def test_pay_success(self, mock_charge): # 让 mock 的 charge 方法返回一个固定结果 mock_charge.return_value {status: success, tid: T123456} result pay_for_order(O001, 99.0) self.assertTrue(result) # 验证 charge 确实被调用了一次且参数正确 mock_charge.assert_called_once_with(order_idO001, amount99.0) mock.patch(my_payment.charge) def test_pay_failed(self, mock_charge): mock_charge.return_value {status: failed, error: insufficient_balance} result pay_for_order(O001, 99.0) self.assertFalse(result)这段代码有几个关键点值得深挖。第一mock.patch的路径必须指向对象被使用的位置而不是定义的位置。例子中my_payment.charge是在pay_for_order模块里被调用的patch的路径就应该写my_payment.charge如果写成payment_service.charge假设的库内部路径是mock不上的。第二当mock方法被patch后被测试函数调用它时不会产生任何真实网络请求而是直接返回你设置的return_value。第三mock_charge.assert_called_once_with(...)能校验“我的被测代码有没有用正确参数调用外部接口”这比你检查返回值更能暴露逻辑问题。Mock还有更进阶的玩法比如用side_effect模拟函数抛异常、模拟连续多次调用返回不同结果、用spec参数限制mock对象只能访问真实对象的属性等。但最核心的认知是mock不是你测试的目的而是你隔离测试环境的手段。mock用得过多测试会变成一个“自己验证自己”的笑话——你把函数内依赖全部mock掉测出来的只是纯逻辑片段而不是真实行为但完全不mock测试又会因为网络抖动、数据变化而变得不稳定。这中间的平衡需要你在实际项目中反复拿捏。4.2 参数化测试的几种姿势写测试用例时你会遇到一个经典场景同一个逻辑需要不同输入组合来验证比如上面的calculate_total_price你可能要测“普通订单”“优惠码订单”“会员订单”“优惠码会员”“空购物车”“购物车只有一件商品”等等。如果每个场景都写一个方法代码会变得又臭又长如果写成循环断言失败时又很难定位是哪一组数据挂了。unittest里最原生的解决方案是subTestclass TestCalculateWithCases(unittest.TestCase): def test_price_cases(self): cases [ ([] , None, False, 0.0), ([{price: 100, quantity: 1}], None, False, 100.0), ([{price: 100, quantity: 1}], SALE2024, False, 80.0), ([{price: 100, quantity: 1}], None, True, 90.0), ([{price: 100, quantity: 1}], SALE2024, True, 72.0), ] for items, coupon, member, expected in cases: with self.subTest(itemsitems, couponcoupon, membermember): self.assertEqual(calculate_total_price(items, coupon, member), expected)subTest的关键价值在于循环中某个case失败时它不会中断整个循环而是会继续跑完所有子用例并在报告里精确显示是哪个子上下文失败了。比如输出会指向items[...] couponNone memberTrue这条。没有subTest时若第三组数据失败后面的数据全都不跑了而且失败信息里不一定能看到输入参数。用第三方库parameterized需要pip安装parameterized也能实现类似效果装饰器风格更简洁from parameterized import parameterized class TestCalculateWithParam(unittest.TestCase): parameterized.expand([ ([{price: 100, quantity: 1}], SALE2024, False, 80.0), ([{price: 100, quantity: 1}], None, True, 90.0), ]) def test_order_price(self, items, coupon, member, expected): self.assertEqual(calculate_total_price(items, coupon, member), expected)注意parameterized.expand会把每个参数组合展开成一个独立的测试项报告里能看到类似test_order_price_0、test_order_price_1这样带序号的方法名。我个人在unittest里的习惯是简单组合用subTest复杂参数组合多且需要独立报错时或者要和pytest风格对齐时才考虑引入parameterized库。另外如果你的项目已经全面转向pytest了pytest的pytest.mark.parametrize就是更主流的参数化方案unittest里虽然也能用pytest跑但写法上会有一些微妙的限制。4.3 测试覆盖率与如何衡量测试是否合格测试写了不少到底够了没这个问题没有绝对答案但覆盖率工具能给你一个量化的参考。Python生态里最常用的覆盖率工具是coverage.py。安装后用它在unittest外面包一层就能统计出代码里被测试执行到的行数占比pip install coverage coverage run -m unittest discover -s ./tests coverage report -m输出大概是这个形式Name Stmts Miss Cover Missing ------------------------------------------- order.py 12 2 83% 45-46 test_order.py 30 0 100% ------------------------------------------- TOTAL 42 2 95%看到Cover: 83%时不要急着高兴也不要急着追求100%。覆盖率高不一定代表测试质量好——你可能只测了函数的正常路径异常路径和边界条件完全没覆盖。我遇到过很多项目测试覆盖率90%以上但线上依然出bug原因就是覆盖到的都是“happy path”。衡量测试是否合格我一般会同时看三个维度行覆盖率被测代码的每一条可执行语句是否被跑到这是基础参考。分支覆盖率if/else、循环、异常分支是否都被跑到。行覆盖率100%分支覆盖率可能只有60%这个更贴近真实逻辑复杂度。变异测试思想为了测试某个特定行为你是否真的断言了关键效果而不是只“execute”了代码没“assert”结果。所以我的建议是把覆盖率当成下限而不是上限。先把核心业务函数的分支逻辑尽量全覆盖然后把测试重点放在“高风险、高变化、高频使用”的代码上。单元测试框架只是你的工具测试策略才是你真正的护城河。5. 实际开发中常见的坑与排查思路5.1 测试之间相互影响的经典陷阱单元测试理论上应该相互独立但实际代码里总会出现“跑单个用例全通过跑全量就挂”的诡异情况。我在这类问题上栽过很多次总结下来主要有三类根因。第一类共享可变状态。比如某个模块级变量或者全局字典被一个测试用例改了内容下一个用例读到的就是脏数据。解决思路在setUp里重新初始化被测对象而不是在类声明时初始化一次如果被测代码本身有缓存机制在tearDown里显式清缓存。第二类依赖执行顺序。某个用例隐式假设自己跑在另一个用例之后。这种问题最隐蔽因为单个跑永远发现不了。解决办法是用unittest的--random模式打乱执行顺序需要手动指定随机种子或者单独跑python -m unittest test_a test_b来对比一旦发现“单个跑和一起跑结果不一样”基本可以断定存在顺序依赖。第三类资源泄漏。比如某个用例建立了数据库连接但没关闭后续用例为了连接池容量被迫等待或报错。排查方法很笨但有效在每个用例前后打印资源使用状态看是否异常增长。这类问题用addCleanup或上下文管理器能很大程度上缓解。提示当全量测试失败而单个测试用例通过时优先怀疑共享状态和资源泄漏。先把测试类的setUpClass改回setUp往往能快速定位。5.2 时间、随机数、网络请求相关测试怎么处理被测代码里经常有“依赖当前时间”“依赖随机数”“依赖网络响应”的逻辑这类代码如果直接编测试会像在沙滩上打地基——今天过了明天同一份代码就可能失败。举几个实际场景。时间相关。假设函数是“判断当前时间是否在工作时段”如果不加处理测试结果会取决于你真实运行测试的时刻。解决办法是让被测函数接收时间参数或者用mock.patch来修改被测代码里依赖的时间来源from unittest import mock from datetime import datetime def is_business_hours(nowNone): now now or datetime.now() return 9 now.hour 18 class TestBusinessHours(unittest.TestCase): mock.patch(main.datetime) def test_morning(self, mock_datetime): mock_datetime.now.return_value datetime(2024, 6, 1, 9, 0, 0) self.assertTrue(is_business_hours()) mock.patch(main.datetime) def test_evening(self, mock_datetime): mock_datetime.now.return_value datetime(2024, 6, 1, 20, 0, 0) self.assertFalse(is_business_hours())这里有个细节为什么是patchmain.datetime而不是datetime.datetime因为代码里写的是from datetime import datetime这时在main模块里引用的datetime是datetime.datetime类的引用patchmain.datetime才能把这个引用替换掉。如果你在业务代码里写的是import datetime然后用datetime.datetime.now()那patch的路径就要改成main.datetime.datetime。这类“引用路径”问题是mock最常见的使用错误。随机数相关。比如抽签算法里用了random.choice要测试“抽中某个特定对象”就需要先控制随机结果可以用mock.patch(random.choice, return_value目标对象)来锁定。网络请求相关。除了前面说的mock掉整个API函数还有更细的做法是mockrequests.get的返回值mock.patch(requests.get) def test_fetch_data_success(self, mock_get): mock_get.return_value.status_code 200 mock_get.return_value.json.return_value {key: value} result fetch_data(http://example.com/api) self.assertEqual(result[key], value)总之处理这类依赖的核心思路不是“让测试绕过真实环境”而是“把不稳定的依赖变成可控的输入”。一旦依赖可控测试用例就从“看运气”变成了“可复现”这才是单元测试该有的样子。5.3 高频踩坑速查表最后整理一份在实际项目中反复出现的高频问题速查表你可以直接当手册用现象原因解决方案单个用例通过全量运行失败用例间共享状态或存在顺序依赖检查全局变量、模块级状态、类级状态尽量用setUp重新初始化浮点数比较偶发失败二进制浮点精度问题用assertAlmostEqual替代assertEqualmock没有生效patch路径指向了定义位置而不是使用位置改为patch被测代码里引用实际对象的模块路径setUp里创建的资源无法释放用例执行中异常导致tearDown未运行用addCleanup注册清理函数或用上下文管理器新增了一个test方法但没被执行方法名没有以test开头检查命名必须以test开头才能被TestLoader收集测试文件很多但discover找到的数量少discover默认模式只匹配test*.py用-p参数指定更宽泛的匹配模式如*_test.py测试大量依赖外部API导致慢和不稳定没有隔离外部依赖用mock.patch、responses库等工具模拟外部调用覆盖率很高但bug仍多覆盖的是“执行路径”而非“断言结果”关注分支覆盖率强化错误分支和边界值测试这个速查表不是标准文档而是我这些年把一个个线上事故、回归异常、CI红叉最后追根溯源得到的总结。每一条背后都有具体的场景你遇到类似问题可以优先对照排查。单元测试框架本身的API并不复杂复杂的是“如何设计出稳定、有效、有意义的测试套件”——这个能力必须靠项目实战慢慢磨出来。我个人在实际操作中的体会是unittest这套框架的曲线其实非常平缓你不需要额外依赖就能开始最初的半小时就能写完第一批用例并跑起来。但真正把它用好需要你对被测代码的结构、外部依赖边界、测试数据设计都有清晰的认知。很多人觉得unittest写起来比pytest啰嗦我倒觉得这种啰嗦反而是一种纪律——它逼着你在一个类里把相似用例组织清楚逼着你显式地声明setUp和tearDown而不是靠装饰器暗地里做魔法。最后再分享一个小技巧如果你决定从unittest迁移到pytest不需要推翻重写pytest本身就能直接运行unittest的TestCase类但反过来理解把unittest的这套设计逻辑吃透了你写pytest用例时对Fixture作用域、参数化、mock时机的掌握都会有更扎实的根基。先老老实实把unittest玩明白再决定要不要“更现代”的框架这条路不会亏。
返回列表