ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python单元测试入门:从unittest到Mock,给代码上保险

Python单元测试入门:从unittest到Mock,给代码上保险 1. 为什么代码需要一份“保险”我常跟刚入门Python的朋友说一句话写代码一时爽改代码火葬场。尤其是当你写完一个函数觉得逻辑没问题、运行也通过了就急着往项目里塞结果过了两周你自己都忘了当初为什么这么写再一改满屏报错——这种经历基本每个写代码的人都撞过。单元测试Unittest干的事就是给代码上“保险”。你给项目里的每个函数、每个类、每个关键逻辑写一份“保单”保单上写清楚输入什么数据应该输出什么结果遇到异常该怎么处理边界情况是否兜得住。之后每次改代码跑一遍这些测试就像体检一样哪里出了问题它第一时间告诉你。你不用等到上线被用户骂了才发现bug也不用在改完一行代码后提心吊胆地怕碰坏别的地方。标准库里的unittest是Python自带的测试框架不需要装任何第三方包学起来成本极低而且是很多大型项目、开源库默认使用的底层测试底座。就算你后来用pytest这类更花哨的工具底层理解和unittest打下的基础也是通用相通的。这篇内容适合所有学过Python基础语法、写过几个脚本但还没系统接触过测试的同学也适合那些写过测试但只知道“照着模板抄”却不懂背后原理的人。先说清楚一个容易被误解的事写单元测试不是为了证明你的代码是对的恰恰相反它是为了证明你的代码“在已知的输入下行为符合预期”。它把“我觉得没问题”变成“有依据的确定”。这份保险不仅保你的代码更保你的睡眠质量。2. 整体思路拆解测试到底测什么怎么测2.1 什么是“单元”为什么不是“整段程序”单元测试里的“单元”指的是代码里最小的、可独立验证的功能块。在Python里通常就是一个函数或者一个类的方法。比如你写了一个计算订单总价的函数里面有折扣逻辑、运费逻辑这就是一个典型的可测单元。为什么不直接对整个程序跑一遍测因为程序是多个单元协作的结果一旦出错你只能看到表面现象——比如“下单失败了”但到底是价格算错了、库存扣错了、还是数据库连接断了你会陷入大海捞针。把每个单元单独拎出来测一旦失败你立刻知道是哪个环节出了问题。这就好比排查一辆车为什么不跑你不可能一上来拆整个发动机肯定先看油、再看电、再看火花塞一个一个部件排查。所以写单元测试的第一思维是学会“拆分”。你的函数写得越单一、越纯粹测试起来越容易。如果一个函数又读文件、又发网络请求、又算业务逻辑那测试它就像在雷区里走路任何一环外部环境变化都会让测试失败而失败的原因可能根本不是你的逻辑错了。这也是为什么很多人写了测试反而觉得“测试老挂没法用”——多半是函数设计得太耦合不纯粹。2.2 测试的三个层次覆盖正常路径、异常路径、边界条件我在带新人写测试时总会让他们把一个功能分成三种场景来思考。正常路径是用户最常规的操作。比如一个登录函数输入正确的账号密码应该返回成功。这是最基础也最容易写的测试。异常路径是用户手滑或者恶意操作的情况。输错密码、传一个空字符串、传一个None函数能不能给一个符合预期的反馈是抛出异常还是返回约定好的错误码还是打印错误日志你写代码时怎么设计的测试就按设计来验证。这里尤其要提醒很多人只测正常路径觉得反正正确情况没问题就行——这话错得离谱生产环境里90%的崩溃都发生在异常路径上。边界条件是取值的最边缘状态。比如你写一个函数接收年龄参数1到120算合法。那0、121、负数、浮点数传进来会怎样再比如列表切片空列表、只有1个元素的列表、超大的列表性能怎么样边界条件最能暴露一个函数的真实健壮性。这三种场景写下来一个函数通常会产生3到8个测试用例。你会发现写测试的时间有时比写函数本身还长但从长期收益看这笔投资非常划算因为bug晚发现一天定位成本可能翻好几倍。2.3 unittest的核心机制TestCase、断言与测试发现unittest的设计理念和使用套路非常固定理解四个概念就够了。TestCase类你的所有测试逻辑都写在一个继承自unittest.TestCase的类里类的每个test_开头的方法就是一个独立的测试用例。断言方法比如assertEqual判断两个值是否相等、assertTrue判断是否为True、assertRaises判断是否抛出指定异常。它们是测试的“裁判”你的代码运行结果符不符合预期由断言说了算。需要特别指出unittest的断言方法比Python内置的assert更适合做测试因为前者失败时会生成详细的对比信息两个值分别是什么后者只会抛一个干巴巴的AssertionError排错效率天差地别。测试夹具Fixture用setUp和tearDown方法管理测试前后的准备与清理工作。比如每个测试前需要创建临时文件、连接数据库测试后把它们删掉、关掉。后面我专门会说这块的坑。测试发现Test Discovery不用手动一个一个跑测试unittest能自动扫描目录下所有符合命名规则的测试文件把它们批量执行。配合命令行运行整个项目的回归测试只需要一条命令。这也是为什么即使有更时髦的pytestunittest依然活得很好——它简单、可靠、够用。到这里你已经掌握了写测试的“世界观”接下来我把“方法论”铺开一步步带你写一个真实可跑的示例。3. 第一个测试用例从0到1完整落地3.1 先准备一个待测模块工欲善其事必先利其器。我们先写一个最简单的业务函数别整花活就用一个购物折扣计算器来演示。假设规则是满100减10满200减30满300减50不满足条件不打折单价可能是小数传入负数或非法类型时抛出ValueError。# shopping.py def calculate_discount(total): 根据订单总金额计算折扣后的应付金额。 规则 - 满100元含减10元 - 满200元含减30元 - 满300元含减50元 - 不满100元不打折 - 非数值类型或负数时抛出 ValueError if not isinstance(total, (int, float)): raise ValueError(total must be a number) if total 0: raise ValueError(total cannot be negative) if total 300: return total - 50 if total 200: return total - 30 if total 100: return total - 10 return total这个函数逻辑简单清晰但里面已经有三个可以测的点正常折扣档位、边界档位、异常输入。如果你愿意可以再加一个“浮点数精度”的测试点因为金额计算涉及小数时直接比较浮点数相等有时会踩精度坑这个细节我在防坑部分会展开说。3.2 编写测试文件跑通第一个用例在同一个目录下建一个测试文件推荐命名规则是test_开头比如test_shopping.py。这样unittest的自动发现机制能直接找到它。这是约定不是装饰。我见过不少人把测试文件命名为shopping_test.py也能手动跑但自动发现就扫不到了后面很麻烦。import unittest from shopping import calculate_discount class TestCalculateDiscount(unittest.TestCase): def test_no_discount_below_100(self): result calculate_discount(99) self.assertEqual(result, 99) def test_discount_100_grade(self): result calculate_discount(100) self.assertEqual(result, 90) def test_discount_200_grade(self): result calculate_discount(230) self.assertEqual(result, 200) def test_discount_300_grade(self): result calculate_discount(350) self.assertEqual(result, 300) def test_invalid_negative(self): with self.assertRaises(ValueError): calculate_discount(-50) def test_invalid_type(self): with self.assertRaises(ValueError): calculate_discount(abc) if __name__ __main__: unittest.main()在命令行执行python test_shopping.py你会看到类似这样的输出...... ---------------------------------------------------------------------- Ran 6 tests in 0.001s OK第一行那六个点代表六个测试全部通过。如果有失败点就会变成F如果出错比如代码本身抛了未捕获的异常会变成E。跑通这一条你已经完成了从“写代码”到“给代码上保险”的第一步转变。3.3 理解测试运行的生命周期你可能会好奇unittest.main()底层到底做了什么其实它的流程可以拆成四步扫描当前模块中所有TestCase的子类收集这些类里所有以test开头的方法每个方法打包成一个测试用例按顺序执行每个测试用例默认按方法名排序汇总每个用例的结果通过/失败/出错/跳过打印报告。这个机制有一个重要推论同一个测试类里各个test_方法之间的执行顺序不受代码书写顺序影响而是按方法名字母排序。所以如果你在A测试里创建了一个文件想在B测试里复用它——千万别这么干因为无法保证A一定先跑。测试之间必须互相独立这是写测试的黄金法则。我后面还会反复强调这条。按这样的结构去写测试代码清晰、易维护、可复用。现在你已经有了第一个可运行的示例但这个示例还太“平滑”了真正的测试世界里充满了各种你看一眼就会踩进去的坑。接下来进入核心实操深度拆解。4. 断言的艺术不止是“相等”那么简单4.1 常用断言方法盘点与选择断言是测试的灵魂但很多人一上来就只会用assertEqual遇到“判断是否抛异常”“判断字符串是否包含子串”“判断两个列表是否内容相同但顺序无关”这些场景就卡住了。这里我把日常最常用的断言方法整理成一个速查表你直接对着用就行。断言方法检查内容典型使用场景assertEqual(a, b)a b数值、字符串、计算结果比较assertNotEqual(a, b)a ! b确认两个值确实不同assertTrue(x)x为真布尔标志、条件成立assertFalse(x)x为假布尔标志、条件不成立assertIs(a, b)a is b比较对象身份内存地址assertIsNone(x)x is None确认返回值是空assertIn(a, b)a in b子串、元素包含关系assertNotIn(a, b)a not in b确认不包含危险元素assertAlmostEqual(a, b)浮点数近似相等规避浮点精度问题assertRaises(Exc)抛出了指定异常异常路径验证assertIsInstance(obj, cls)类型匹配返回值类型校验这里我要重点讲两个高频但不被重视的。第一个是**assertRaises的正确写法**。很多人这样写# 错误示范 try: calculate_discount(abc) except ValueError: pass # 只要没崩就认为通过了这确实是“捕获了异常”但它捕获后什么都没验证哪怕函数因为别的原因抛了异常它也会通过也就是说断言形同虚设。正确写法是用上下文管理器with self.assertRaises(ValueError): calculate_discount(abc)这样写不仅断言了“确实抛出异常”还可以确保异常发生在指定的代码块里。如果你还想进一步验证异常消息的内容提醒好好做这个异常消息是排查问题的关键线索它还有一个别名用法with self.assertRaises(ValueError) as ctx: calculate_discount(abc) self.assertEqual(str(ctx.exception), total must be a number)第二个是浮点数比较精度问题。你算一个3 * 0.1在Python里得到的是0.30000000000000004而不是0.3。如果测试里写assertEqual(3 * 0.1, 0.3)必然失败而且失败得很冤。正确做法是用assertAlmostEqual它允许两个数在指定的小数位数内近似相等。self.assertAlmostEqual(0.1 0.2, 0.3, places7)金额计算这类场景特别容易踩这个坑建议处理金额先用“分”为单位用整数存储从根上规避精度问题如果偷懒用浮点数断言时务必用assertAlmostEqual。4.2 一个断言失败测试为什么立即停止很多初学者会奇怪一个test_方法里有三行断言第一行失败了后面两行为什么不执行了这是unittest的既定设计——每个测试用例应当是原子的一个断言失败就代表这个场景不通过继续执行后面的断言没有意义反而可能因为状态已经被破坏了而引发连锁错误。那是不是说一个test_方法里只能写一个断言也不是。多个断言前后之间如果互不影响可以放在一起。比如你测一个返回元组的函数同时验证长度和内容这两个断言就属于同一场景的互补验证放一起没问题。如果你发现自己在一个测试里写了七八个断言而且失败后信息互相纠缠就该拆成多个test_方法了。举一个容易让人迷惑的实际例子经验不足时我猜很多人写过这样的“伪断言”——测一个函数返回了列表就assertTrue(result)。这个断言只要result是非空的列表就通过了但如果它本应返回[1,2,3]却返回了[a,b]assertTrue完全发现不了。所以断言的选择必须和你的验证目的严格匹配。想验证列表内容就用assertEqual和完整期望值想验证列表长度就用assertEqual(len(result), 3)想验证顺序不敏感的存在关系用assertCountEqual。测试的准确程度完全取决于你对断言的考究程度。5. 测试夹具FixturesetUp和tearDown的正确时机5.1 什么时候必须用夹具什么时候别硬凑setUp方法会在每个测试用例执行前自动调用tearDown在每个用例执行后调用。听起来很方便于是有人把什么逻辑都往里面塞塞完发现测试变慢、相互干扰、还不好排查。我见过最夸张的情况有人在setUp里启动了完整的Web服务每个测试跑一次整份测试跑下来耗时十几分钟。这里我给一个实用判断标准只有当每个测试用例执行前都需要一份“一模一样的准备条件”并且这份准备成本比较可观时才用夹具。典型场景有每个测试前创建一个数据库连接并清空表数据、每个测试前构造一个复杂的配置字典、每个测试前创建临时文件。反例不该用夹具的情况某些测试需要数据库里有不同状态的数据那你就不应该在setUp里统一灌一份相同的数据正确的做法是每个测试方法自己准备自己需要的数据或者用setUp只搭“空壳框架”数据在测试方法内补。5.2 完整示例带临时文件的测试演示一个场景被测函数读取文本文件并统计单词数量。import os import tempfile import unittest def count_words(filepath): with open(filepath, r, encodingutf-8) as f: text f.read() return len(text.split()) class TestCountWords(unittest.TestCase): def setUp(self): # 每个测试用例运行前创建一个临时文件并写入一行内容 self.temp_dir tempfile.TemporaryDirectory() self.file_path os.path.join(self.temp_dir.name, sample.txt) with open(self.file_path, w, encodingutf-8) as f: f.write(hello world python unittest) def tearDown(self): # 每个测试用例运行后清理临时目录 self.temp_dir.cleanup() def test_count_normal(self): result count_words(self.file_path) self.assertEqual(result, 4) def test_count_empty(self): with open(self.file_path, w, encodingutf-8) as f: f.write() result count_words(self.file_path) self.assertEqual(result, 0)这个示例里setUp保证了每个测试开始时都有一个人工可控的初始文件test_count_empty里自己覆盖了文件内容因为它的初始状态需要“非空文件”——这个差异化准备放在测试方法内部而不是setUp里就是前面说的原则落地。注意一个细节tearDown里我调用了cleanup()如果忘记这个调用每次测试都会留下临时目录残骸跑个几十次测试后你的系统临时文件夹里全是你测试的“尸体”。现在Python 3.8的TemporaryDirectory有了更优雅的替代写法class TestCountWords(unittest.TestCase): def setUp(self): self.temp_dir_obj tempfile.TemporaryDirectory() def tearDown(self): self.temp_dir_obj.cleanup()用with tempfile.TemporaryDirectory() as td:的方式在setUp里实现也可以with块结束会自动清理更稳妥。5.3 类级别的夹具setUpClass与tearDownClass如果某项准备逻辑对所有测试用例只需要做一次比如连接数据库、下载一个较大的测试数据集那么把它放进setUpClass。这个类方法在测试类的执行周期里只运行一次能帮你省下大把时间。class TestDatabase(unittest.TestCase): classmethod def setUpClass(cls): cls.connection create_db_connection() cls.connection.initialize_schema() classmethod def tearDownClass(cls): cls.connection.close()这里有个极易踩的坑setUpClass抛异常时测试类里所有用例都会失败而且失败信息可能指向“没有连接”让你误以为是测试本身的问题。所以在setUpClass里做的操作要确保它们自身的健壮性该加的日志加日志该做的检查别漏。另一个注意点是setUpClass里创建的连接是共享的如果某个测试修改了共享连接的状态比如事务没提交会污染后续测试。这违背了我之前说的“测试之间必须互相独立”原则所以能用setUp别硬用setUpClass优先保证独立性再考虑效率。6. 测试隔离与Mock把外部依赖“关进笼子”6.1 为什么测试不能依赖真实网络和真实时间到了实战阶段你的函数多半会涉及网络请求、数据库操作、当前时间、随机数、环境变量。这些外部依赖有一个共同点——它们的输出不受你控制。你今天测一个获取天气的函数接口返回晴天测试通过明天接口抽风返回了500测试就失败了。但是你的业务逻辑代码一行没改这个失败该怪谁所以测试领域有一条铁律测试环境必须可控。可控的意思是被测代码里的外部调用在测试期间要用假的替身代替这个替身在测试领域叫“测试替身”或“Mock对象”。有人会觉得Mock了之后测的还有意义吗会不会测了一个假的这里要掰开揉碎讲清楚你测的不是“外部服务”你测的是自己代码在外部服务返回特定结果时的处理逻辑。比如你写了一个函数调用天气API后根据响应决定是否带伞。你要验证的不是“天气API真的会下雨”而是“当API返回下雨数据时我的函数能正确提示带伞”。外部的正确性与你的代码无关你的代码在已知输入下行为正确这才是单元测试的边界。6.2 mock.patch的基本玩法与坑标准库里内置了unittest.mock模块最常用的接口是patch。看一个例子# weather.py import requests def check_umbrella(city): response requests.get(fhttps://api.example.com/weather/{city}) data response.json() if data.get(rain) is True: return 带伞 return 不用带伞 # test_weather.py from unittest import mock, TestCase from weather import check_umbrella class TestCheckUmbrella(TestCase): mock.patch(weather.requests) def test_rainy_day(self, mock_requests): fake_response mock.Mock() fake_response.json.return_value {rain: True} mock_requests.get.return_value fake_response result check_umbrella(北京) self.assertEqual(result, 带伞)这段代码里有三个关键点一个是patch的目标路径必须和被测代码里“实际使用的位置”一致而不是定义位置。这里weather.py里执行的是requests.get所以patch的路径是weather.requests如果你patch的是requests.get会发现没有生效因为weather模块里的requests名字已经被绑定成对原模块的引用了。这条是mock最经典的坑十个人里至少四个人踩过。另一个是fake_response的链式调用。mock.Mock()对象上访问任何属性、调用任何方法都会返回新的Mock不会报错直到你给它设定返回值。我这里用.json.return_value设定了json()方法的返回值。如果你没设置这个返回测试运行时会得到另一个Mock对象断言必然失败而且报错信息看起来像是“预期dict实际mock”很多人就会被带偏以为问题是出在mock上而非返回值设置上。第三个是被patch后的对象assert_calls可以验证调用次数与参数mock_requests.get.assert_called_once_with(https://api.example.com/weather/北京)这能确认你的代码确实按预期调用了外部接口而且参数没传错。6.3 临时替换时间的mockfreeze time处理依赖当前时间逻辑的代码比如优惠券是否过期、任务是否超时最常用的手段也是mock.patch直接把时间源替换成一个固定值from unittest import mock from datetime import datetime def is_expired(expire_at: str) - bool: now datetime.now() expire datetime.fromisoformat(expire_at) return now expire mock.patch(datetime.datetime) def test_is_expired(self, mock_datetime): mock_datetime.now.return_value datetime(2025, 1, 1, 12, 0, 0) self.assertTrue(is_expired(2024-12-31T23:59:59))这里会有一个大家绕不过去的坑datetime是Python内置类patch(datetime.datetime)后测试方法里所有用到datetime的地方都会变成Mock。如果你在测试体中直接调用datetime(2025, 1, 1, 12, 0, 0)它会失败——因为现在的datetime已经是个Mock了。标准解法是先用from datetime import datetime as real_datetime把真实的类保存一份然后在Mock里引用它。7. 测试套件与自动化从单文件到全项目一键跑7.1 用TestSuite手动组织测试前面文件里的unittest.main()只在单文件内部生效。真实项目里测试文件动辄几十上百个你需要一把“总钥匙”。TestSuite可以手动把零散的测试类聚合起来。你可能会觉得这不就是把多个文件里的测试放进一个列表吗确实是这样但手动组织有一个额外的好处你可以指定执行顺序还可以按产品模块分批执行。比如要只跑“订单模块”的测试就不必全项目跑一遍节省大量时间。# run_tests.py import unittest from test_shopping import TestCalculateDiscount from test_weather import TestCheckUmbrella def suite(): loader unittest.TestLoader() suite unittest.TestSuite() suite.addTests(loader.loadTestsFromTestCase(TestCalculateDiscount)) suite.addTests(loader.loadTestsFromTestCase(TestCheckUmbrella)) return suite if __name__ __main__: runner unittest.TextTestRunner(verbosity2) runner.run(suite())verbosity2会让每个测试方法的名字都打印出来跑挂时你能一眼定位到具体是哪个用例挂了。7.2 使用test discovery自动收集全部测试手动组织虽然灵活但文件一多就变得很难维护新增一个测试文件就得改一次run_tests.py。unittest提供的自动发现机制解决的就是这个问题。只要你的测试文件都以test*.py命名并且都在项目目录下或指定起始目录下一条命令就能全跑python -m unittest discover -s tests -p test_*.py -v-s tests指定从tests目录开始扫描-p test_*.py指定匹配的文件名模式-v打印详细输出。这条命令的好处是不需要任何额外的“汇总文件”新增测试文件后直接就能被发现并执行回归成本趋近于零。7.3 测试覆盖率保险上了但上得够不够跑完测试、看到全部通过别急着高兴。你得问一句这些测试到底覆盖了代码的多少分支覆盖率这个概念的作用是告诉你“哪些代码从未被执行过”。如果你写了个函数里面有个if分支但你的测试设定里永远走不到那个分支那么你有代码在“裸奔”将来很可能就在那个分支上炸。用coverage.py可以直观地把这些裸露区域标记出来。pip install coverage coverage run -m unittest discover -s tests coverage report -m coverage html # 生成详细HTML报告我第一次跑覆盖率时心态崩了自以为测试写得挺全结果一看报告只有63%。后来补了几条边界条件的用例涨到了88%心里才踏实一点。覆盖率不是越高越好100%的行覆盖不代表没有逻辑漏洞但如果你连80%都不到那说明测试的保险意识还远不够。我自己的经验是核心业务逻辑尽量打到90%以上工具类、外部对接层可以适当放宽。8. 常见问题与排查技巧实录8.1 测试通过了但代码实际还是有问题这是最让人绝望的场景。测试全绿上线就崩。我复盘过好几次主要原因基本逃不开三类一是测试数据和真实数据格式不一致。测试时构造的dict结构和真实接口返回的结构有细微差别比如字段名大小写、嵌套层级、空值处理。这要求你造数据时尽量贴近“真实样本”最好的办法是线上抓一份脱敏真实请求作为测试数据源。二是只测了“晴天路径”。真实逻辑里大量异常分支、超时分支、重试逻辑完全没有被触发自然测不出问题。这时覆盖率报告能帮上忙——去看看哪些分支没有跑。三是测试环境本身和生产环境有差异。比如本地数据库是SQLite生产是MySQL本地跑的是Python 3.11服务器是3.8。这不算单元测试的锅但你要意识到测试结果的有效性和环境一致性紧密相关。8.2 测试之间互相影响、跑单个通过、全量跑失败这个场景很像“薛定谔的测试”——单独跑一个文件全绿全项目一起跑就挂。几乎都是因为测试污染了共享状态。常见污染源测试修改了环境变量但没有恢复测试创建了同名临时文件没清理干净就退出测试连接了共享数据库A用例改了数据B用例读到了脏数据测试使用Mock时patch的模块被后续用例引用了。我自己踩过最狠的一次是某个测试给os.environ设了一个值没有在tearDown里还原结果另一个模块的初始化逻辑在导入时读取了这个被污染的环境变量导致全量测试从第十个用例开始成片失败。排查了一个下午才定位到是环境变量污染。排查思路首先看“是否与执行顺序相关”把全量失败后第一个失败的用例单独拎出来跑如果单独跑能过基本就是被前序用例污染了。其次检查每个涉及文件、环境变量、数据库操作的测试tearDown务必把状态还原到最初的样子。如果实在还原困难就改用Mock把共享操作直接替换掉从根上杜绝污染。8.3 测试运行慢到无法接受当测试数量超过几百个运行时间就会肉眼可见地增长每跑一次全量测试可能要几分钟甚至几十分钟人会变得不想跑测试测试也就形同虚设。慢的根源通常是两条一条是测试触碰了真实外部服务。即使只有几十个网络请求耗时也会被拉高到秒级再加上网络抖动测试结果还不稳定。解决办法就是前面说的Mock把网络I/O从单元测试里彻底赶出去。就算出于某种原因必须连接真实服务做集成测试也要用unittest.skip机制把这类测试默认跳掉只在特定的部署流程里通过开关去跑。另一条是共享了重资源比如每个测试用例都重新启动进程或建立新的数据库连接。对这类通用资源前面说的setUpClass就能大幅减少开销更进一步的话可以用模块级的setUpModule比类级更“经济”。8.4 测试断言信息不直观失败后还得自己算很多新手写断言喜欢简单粗暴self.assertTrue(result expected)一旦失败只会看到一行AssertionError至于result到底是什么、与expected差多少完全没有输出。调试时等于盲人摸象。正确用法是直接使用带对比信息的方法self.assertEqual(result, expected)失败时unittest会自动展示两个值的内容差异。如果还嫌不够友好可以在断言前加一个自己的提示信息self.assertEqual(result, expected, f计算结果与预期不一致差值为{result - expected})这个细节能极大提升排错速度属于“不加白不加”的好习惯。8.5 测试文件不知道怎么归类当项目越来越大测试文件会越来越多。我的整理习惯是这样项目根目录下建tests目录内部按被测模块再分子目录比如tests/test_order/、tests/test_user/。每个测试文件对应一个被测模块或一个大的功能域。命名统一用test_前缀加模块名比如test_order.py、test_user_service.py。这样做的原因是文件一多目录结构本身就是一种文档——谁负责哪块哪个文件对应哪个模块扫一眼就懂也方便以后按模块做测试筛选。另外测试文件里如果有一些造数据的公共函数别塞进单个测试文件里到处复制建一个tests/utils.py或tests/factories.py专门放公共构造器测试代码的复用性会变得很舒服。9. 实测心得我踩过的坑希望你绕开测试写得多了慢慢会形成一些条件反射。我说几个自己感受最深、也最想提醒后来人的点。第一个体会是测试代码也是代码它同样需要可读性。我见过有人测试写得很“聪明”一个断言里塞了三个if结果测试失败时根本看不出业务意图。好的测试代码其实非常朴素一个用例只做一件事断言消息写得像人话。它甚至比生产代码更需要清晰度因为你半年后回到这个项目跑挂一个测试你要能在一分钟内看懂它到底在测什么、为什么挂了。你现在写的每个清晰用例都是为未来的自己留的救命地图。第二个体会是先写测试再写实现测试驱动开发TDD的人写出来的接口会普遍更好用。为什么因为当你先想测试时你会被迫站在使用者的角度去思考“这个函数入参是什么、返回什么、异常怎么处理”而不是想怎么写省事。这种视角转变导致函数设计更简洁、边界更清楚。我不要求谁都严格TDD但如果你在动手写一个复杂函数前先花五分钟把它的输入输出和异常路径列清楚后期写测试时会惊喜地发现自己漏了好几个用例。第三个体会是关于测试心态的测试跑红了不一定是坏事。它代表系统比你更早知道哪里不对劲重点在于读失败信息时保持冷静从错误输出里定位真实原因而不是急着到处瞎猜。几乎每次跑红耐心看完堆栈都能直指问题所在。第四个也是我很想强调的测试不是写一次就完事的。它和产品代码一样需要维护、需要随功能迭代同步更新。项目里最怕的不是没有测试而是有一堆过期测试——跑起来全绿但其实已经有几个用例失去了意义或者断言的目标已经被改得面目前非。这些“僵尸测试”不仅浪费执行时间还会给你虚假的安全感。我的习惯是每完成一个功能迭代就顺手把相关测试文件整个过一遍删掉过时的补上新增的让测试集永远和当前逻辑对齐。如果你理解了这些并且按上面的步骤动手写过哪怕十个测试用例那么你对Python代码的掌控力和信心已经超过很多写了两三年业务代码但从不碰测试的人。给代码上保险这个事投入不高回报却远超预期而且越早开始越能感受到它带来的踏实感。
返回列表