拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Python单元测试实战:从unittest到Mock与覆盖率

很早就想写一篇关于Python单元测试的实战文章了。原因很简单我带过的项目里凡是测试写得扎实的后面加需求、改逻辑都踏实凡是靠手工验证的迟早被一个改坏的老功能搞得焦头烂额。这篇就围绕Python标准库里的unittest展开从环境准备到核心API再从真实业务场景到Mock、覆盖率、异步测试最后把我踩过的一些坑一并摊开。文章偏实战也照顾刚入门的朋友尽量做到每一步都能跟着操作、跑通。1. 单元测试不是写给别人看的先解决为什么值得做1.1 一次没有测试的代码改动我是怎么崩溃的去年六月我们线上有一个订单积分计算的功能逻辑不算复杂用户下单之后根据订单金额和会员等级算积分等级越高倍数越高。某次产品提了个小需求说部分商品不参与积分返还代码改动就加了一个if判断。听起来很简单对吧我当时偷懒没写测试改完在本地手工试了两个订单看着没问题就直接提交发版了。结果上线第二天就有人反馈某些不参与积分返还的商品之前已经给过积分了改完之后后续订单反而多扣了积分。查了半天发现问题出在不参与积分返还的商品和会员等级加成为0这两个条件在原有逻辑里的组合被我的新if覆盖掉了。那一次事故的直接成本是一个小时的排查、一次紧急修复、十几个用户的人工补偿。间接成本更麻烦——同事开始对那套代码没有信任感每次改动都要手工回归好几轮。事后复盘时大家一致认为最便宜的一步就是为这个函数补上单元测试把订单金额会员等级是否参与返还这些参数的组合都覆盖一遍。如果当时有十几个断言在跑这个bug根本活不过本地开发阶段。那次之后我给自己定了一个规矩凡是写了业务逻辑的函数凡是有人敢改的逻辑必须能跑通一组unittest用例。这不是为了给领导看测试报告是为了让我自己睡前能睡得踏实。1.2 单元测试到底测什么、不测什么很多初学者对单元测试有误解以为它就是把整个系统跑起来点一点页面看看通不通。那不是单元测试准确说是集成测试或者手工冒烟测试。单元测试的核心对象是函数、类、方法这一层的最小逻辑单元重点验证的是给定确定的输入能否得到期望的输出。所以单元测试特别适合捕获这些典型问题条件分支的边界问题比如金额是0、负数、超大数、恰好等于某个阈值字符串处理、类型转换时的意外输入循环和递归的终止条件状态类对象的内部状态迁移是否符合预期外部依赖网络、数据库、时间被替换后业务逻辑是否正确。那单元测试不测什么不测数据库连不连得上、不测第三方接口是否返回200、不测UI的像素是否对齐。这些属于集成测试、端到端测试的范畴强行塞进单元测试里只会让用例变得又慢又脆弱。判断一个用例是不是合格的单元测试有一个很土的标准它能不能在没有任何网络、任何数据库、任何外部服务的情况下毫秒级跑完如果不能就先想想是不是把依赖拆得不够干净。2. 跑通第一个用例环境、目录和最小示例2.1 Python环境和项目环境虚拟环境不是可选项unittest是Python标准库自带的模块理论上你装好Python就能用不需要额外pip install什么。但这不代表你可以忽略环境问题。先说Python版本。不同版本的Python对unittest的行为有些微差异特别是Python 3.11之后有些异常链、上下文管理的表现更规范了。团队协作时建议用.python-version或者requirements.txt把版本固定下来避免我本地是3.9你那边是3.12跑出来的结果不一样这种玄学问题。然后是虚拟环境。很多人开始学Python时图省事直接把包装到全局环境一旦项目多了依赖互相打架是迟早的事。我的习惯是每个项目一个虚拟环境在项目根目录执行python -m venv venvWindows下激活venv\Scripts\activatemacOS/Linux下激活source venv/bin/activate为什么要强调这一步因为你后面跑覆盖率工具coverage、也许还会用到pytest做对比实验这些工具如果装在全局环境会让测试结果受到无关包的影响。虚拟环境隔离之后你的测试环境才是干净可复现的。如果你用的是PyCharm或者VS Code可以在项目设置里把解释器指向venv里的Python。这一步做好之后后面所有命令行操作和IDE运行测试才不会出现解释器不对的怪问题。2.2 测试文件到底放哪里目录结构决定维护成本我见过很多项目把测试文件直接和业务代码堆在一起比如order.py旁边放一个test_order.py。这种做法在小脚本里还行项目一旦超过几十个文件测试会淹没在业务代码里discover的匹配规则也容易出问题。我更推荐的是和unittest官方主流实践一致的结构project/ ├── src/ # 业务代码 │ ├── __init__.py │ ├── order.py │ └── member.py └── tests/ # 测试代码 ├── __init__.py ├── test_order.py └── test_member.py有两个细节值得注意。第一tests目录下一定要有__init__.py文件。因为unittest discover默认是从当前目录递归查找test*.py文件如果目录不是包结构在某些导入场景下会报ModuleNotFoundError。加上__init__.py之后测试文件之间可以用相对导入去复用公共工具。第二测试文件命名必须以test开头比如test_order.py、test_member_utils.py。这是unittest默认的匹配规则python -m unittest discover会去找所有test*.py文件。你要是命名成order_test.py默认规则是找不到的除非你显式指定-p *.test.py没必要给自己找麻烦。2.3 第一个可以运行的TestCase从命令行到IDE现在我们来写一个最小用例。假设有一个简单的加法函数放在src/calculator.py里# src/calculator.py def add(a, b): return a b那么tests/test_calculator.py可以这样写# tests/test_calculator.py import unittest from src.calculator import add class TestCalculator(unittest.TestCase): def test_add_positive_numbers(self): self.assertEqual(add(1, 2), 3) def test_add_negative_numbers(self): self.assertEqual(add(-1, -1), -2) if __name__ __main__: unittest.main()在项目根目录跑python -m unittest discover -s tests正常情况下会看到.. ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK两个点代表两个用例都通过了。如果某一个失败会显示成F并且会打印详细的断言失败信息告诉你期望值和实际值差在哪。从这一步开始你已经摸到了unittest的门槛。接下来我们把核心API过一遍搞清楚它能帮我们做什么。3. unittest核心API用一次就忘不掉3.1 断言方法真的比你想的多很多新手只会用assertEqual和assertTrue一旦遇到需要验证异常是否被抛出列表是否包含某个元素浮点数是否足够接近就手足无措只能写一堆笨拙的try...except。实际上unittest.TestCase内置了丰富的断言方法我常用的整理成了下面这张表。断言方法作用使用场景assertEqual(a, b)判断两个值相等最通用assertNotEqual(a, b)判断两个值不等排除性检查assertTrue(x)/assertFalse(x)判断布尔值条件结果assertIsNone(x)判断是None空结果判断assertIn(a, b)判断a in b成员包含关系assertAlmostEqual(a, b, places6)判断浮点数近似相等计算精度断言assertRaises(SomeException, func, *args)判断是否抛指定异常异常分支测试assertLogs(logger, level)判断是否产生日志日志记录断言assertDictEqual(a, b)判断两个字典相等字典内容比对其中最容易被忽视的是assertAlmostEqual。直接比较两个浮点数非常不可靠self.assertEqual(0.1 0.2, 0.3) # 大概率失败因为0.1 0.2在IEEE 754标准下并不是精确的0.3而是一个很接近的数。这种场景应该用self.assertAlmostEqual(0.1 0.2, 0.3, places6)它允许误差在10^-6以内。以后凡是涉及浮点运算的断言我都建议无脑用assertAlmostEqual至少不会因为精度问题半夜被叫起来。assertRaises也很有用它可以很方便地验证异常分支。比如一个除法函数除数为0时要抛ValueError你可以这么写with self.assertRaises(ValueError): divide(1, 0)还可以通过assertRaises的msg参数或者上下文管理器里的exception对象进一步校验异常信息with self.assertRaises(ValueError) as ctx: divide(1, 0) self.assertEqual(str(ctx.exception), divisor cannot be zero)3.2 setUp/tearDown与setUpClass资源准备和清理的时机测试用例里经常需要准备测试数据、临时文件、连接对象等资源。如果每个用例都写一遍初始化和清理代码会非常冗余。unittest提供了一套生命周期方法class TestOrder(unittest.TestCase): def setUp(self): # 每个测试方法执行前都会执行 self.order create_test_order() self.db tempfile.NamedTemporaryFile() def tearDown(self): # 每个测试方法执行后都会执行 self.db.close() classmethod def setUpClass(cls): # 整个测试类执行前执行一次 cls.global_config load_config() classmethod def tearDownClass(cls): # 整个测试类执行后执行一次 cls.global_config NonesetUp和tearDown适合处理每个用例之间独立的资源比如新建一个临时文件、创建一个新的用户对象。setUpClass和tearDownClass适合处理共享的、耗时的资源比如启动一次数据库连接池、读取一次全局配置文件。这里要特别提醒setUpClass里准备的资源是跨用例共享的如果某个用例意外修改了共享资源后面的用例很可能被污染。所以共享的前提是资源只读或者你确定每个用例用完都能恢复原状。否则宁可放到setUp里每个用例重新创建。我见过最典型的翻车场景是有人在setUpClass里创建了一个全局订单对象第一个测试改了它的状态第二个测试拿到的是脏数据排查了好久才发现是共享对象被改了。遇到这种情况我的建议很简单不知道能不能共享的一律不共享。3.3 TestSuite和discover控制粒度、批量执行和报告unittest.main()和unittest discover适合日常开发阶段。但当你的用例多了需要按指定顺序执行、只跑某些用例、或者集成进CI系统时建议用TestSuite手动组装import unittest from tests.test_order import TestOrder from tests.test_member import TestMember def suite(): test_suite unittest.TestSuite() test_suite.addTest(TestOrder(test_apply_discount)) test_suite.addTest(TestMember(test_calculate_points)) return test_suite if __name__ __main__: runner unittest.TextTestRunner(verbosity2) runner.run(suite())如果要让所有测试文件都能被自动发现用discover更省心python -m unittest discover -s tests -v-v参数会打印出每个用例的名字方便看哪些用例在跑、哪些挂了。我在本地开发时几乎总是加-v。还有一个很实用的小技巧临时只想跑某个文件里的某个用例不用注释代码直接用点路径指定python -m unittest tests.test_order.TestOrder.test_apply_discount这个命令只跑test_apply_discount这一个用例可以帮你把注意力集中在一处。配合-v输出定位问题非常高效。4. 实战给折扣计算逻辑写一组可信赖的测试4.1 需求和边界分析测试用例设计的起点光讲API很抽象我们拿一个完整的业务场景来走一遍。假设现在要写一个订单折扣计算函数需求是这样的函数接收三个参数original_price原价浮点数、member_level会员等级字符串、is_promotion_product是否参与促销布尔值。折扣规则普通会员原价不计折扣金牌会员折扣9折钻石会员折扣8折如果商品本身是促销商品则在会员折扣的基础上再减10元折后价最低不能低于1元防止出现0元或负数订单原价必须大于0否则抛ValueError异常。这种需求如果直接写代码很容易漏掉边界条件。正确做法是先设计测试用例再写实现——也就是测试驱动开发的思路。推荐的用例包括原价为100、普通会员、非促销商品期望折扣后价格100原价为100、金牌会员、非促销商品期望90原价为100、钻石会员、非促销商品期望80原价为100、金牌会员、促销商品期望8090减10原价为100、普通会员、促销商品期望90100减10原价为5、金牌会员、促销商品期望是1而不是0因为5打9折是4.5再减10成负数要兜底到1原价为0或负数期望抛ValueError。把这些用例写下来你会发现很多实现细节被提前逼出来了最低1元到底是在打折之后减10之前设下限还是在最终结果设下限不同理解会导致不同实现。测试用例的本质就是用具体输入把需求和实现约定死。4.2 编码实现测试配套每一步都跑绿先按最直观的理解实现一版# src/order.py DISCOUNT_RATE { normal: 1.0, gold: 0.9, diamond: 0.8, } def calculate_discount(original_price, member_level, is_promotion_product): if original_price 0: raise ValueError(original_price must be positive) if member_level not in DISCOUNT_RATE: raise ValueError(funsupported member_level: {member_level}) price original_price * DISCOUNT_RATE[member_level] if is_promotion_product: price - 10 return max(price, 1)然后为它写对应的测试文件# tests/test_order.py import unittest from src.order import calculate_discount class TestCalculateDiscount(unittest.TestCase): def test_normal_member_no_promotion(self): self.assertEqual(calculate_discount(100, normal, False), 100) def test_gold_member_no_promotion(self): self.assertEqual(calculate_discount(100, gold, False), 90) def test_diamond_member_no_promotion(self): self.assertEqual(calculate_discount(100, diamond, False), 80) def test_gold_member_with_promotion(self): self.assertEqual(calculate_discount(100, gold, True), 80) def test_normal_member_with_promotion(self): self.assertEqual(calculate_discount(100, normal, True), 90) def test_promotion_does_not_make_price_below_one(self): self.assertEqual(calculate_discount(5, gold, True), 1) def test_invalid_price_raises_error(self): with self.assertRaises(ValueError): calculate_discount(0, gold, False) def test_unknown_level_raises_error(self): with self.assertRaises(ValueError): calculate_discount(100, super_member, False) if __name__ __main__: unittest.main()跑一下python -m unittest discover -s tests -v如果全部通过这个函数的核心行为就被锁定了。以后任何人优化代码、重构函数只要这8个用例还是绿的基本不用担心改坏上面这些需求点。4.3 用子类组织相同场景subTest与多组数据上面的例子比较规整但真实项目的函数参数通常更复杂。比如一个函数可能有用户类型、订单状态、是否加急、是否新客好几种维度组合全写成一个一个的测试方法会非常冗余。这时有两个办法。第一个是数据驱动的思路用一个方法加循环def test_multiple_discount_cases(self): cases [ (100, normal, False, 100), (100, gold, False, 90), (100, diamond, False, 80), (100, gold, True, 80), (100, normal, True, 90), (5, gold, True, 1), ] for price, level, promotion, expected in cases: with self.subTest(priceprice, levellevel, promotionpromotion): self.assertEqual(calculate_discount(price, level, promotion), expected)subTest的价值在于如果循环中某组数据断言失败它会把当前参数打印出来而不是整个测试方法中断。比如你可以看到Failed subtests: test_multiple_discount_cases (tests.test_order.TestCalculateDiscount) ... price5, levelgold, promotionTrue这样一眼就知道是哪组输入出了问题排查起来快很多。第二个办法是公共测试基类。当你需要对两个不同的实现执行同一套测试时可以写一个基类class BaseDiscountTest(unittest.TestCase): def create_discount_func(self): raise NotImplementedError def test_discount(self): func self.create_discount_func() self.assertEqual(func(200, gold, False), 180) class DiscountV1Test(BaseDiscountTest): def create_discount_func(self): from src.order import calculate_discount return calculate_discount class DiscountV2Test(BaseDiscountTest): def create_discount_func(self): from src.order_v2 import calculate_discount_v2 return calculate_discount_v2以后重构出新版本只要继承基类补一个工厂方法就能复用整组测试。这种模式在做算法多版本对比、新旧接口兼容性测试时特别好用。5. 外部依赖怎么测Mock与隔离的实用姿势5.1 为什么要mock掉外部调用慢和随机是测试的天敌真实项目中函数往往不是纯逻辑它还会调用外部接口、读写数据库、获取当前时间。如果把那些调用原样带进单元测试会带来两个严重后果一是慢。每次跑用例都真的去请求一次网络一个用例就要几百毫秒甚至几秒几百个用例跑下来几分钟就没了。程序员一不耐烦就不跑测试了测试就形同虚设。二是不稳定。外部接口偶尔超时、数据库偶尔连不上、哪天第三方接口改了返回结构你的业务代码没变测试却红了。这种假失败会让团队逐渐对测试结果失去信任。所以单元测试的黄金法则是测试只关心被测函数的内部逻辑外部依赖通通替换成可控的替身。unittest.mock就是Python标准库提供的替身工具。5.2 mock一个网络请求和数据库对象假设你有一个get_user_points(user_id)函数它内部会调用一个HTTP接口获取用户积分# src/user_service.py import requests def get_user_points(user_id): resp requests.get(fhttps://api.example.com/users/{user_id}/points, timeout3) resp.raise_for_status() return resp.json()[points]如果直接测试这个函数你必须有网络环境还要保证第三方接口可用显然不现实。正确的做法是mock掉requests.get# tests/test_user_service.py from unittest import mock import unittest import requests from src.user_service import get_user_points class TestGetUserPoints(unittest.TestCase): mock.patch(src.user_service.requests.get) def test_success(self, mock_get): mock_resp mock.Mock() mock_resp.raise_for_status.return_value None mock_resp.json.return_value {points: 100} mock_get.return_value mock_resp self.assertEqual(get_user_points(42), 100) mock.patch(src.user_service.requests.get) def test_http_error(self, mock_get): mock_get.side_effect requests.HTTPError(500 Server Error) with self.assertRaises(requests.HTTPError): get_user_points(42)这里的关键点是mock的对象必须是指向被测模块里实际引用的名字空间。get_user_points里调用的是requests.get它在src.user_service这个模块里被引用所以我要mock的是src.user_service.requests.get而不是requests.get本身。如果mock错了地方比如写成了mock.patch(requests.get)多数情况下不会生效因为被测模块里引用的是它自己命名空间中的requests.get那个指向还是原来的真实函数。5.3 patch的常见错法patch错了地方等于没测我见过很多刚接触Mock的同学在这个地方栽跟头浪费一两个小时。总结下来有三种典型错法。第一种patch了业务代码根本没用的对象。比如业务代码用的是from requests import get你却去mock.patch(requests.get)这是不管用的。因为from requests import get之后函数名get已经被绑定到了当前模块的命名空间你改外层requests模块的get已经影响不到它了。正确做法是patch当前模块的get。第二种在装饰器列表里搞错了参数顺序和名称。用mock.patch装饰测试方法时mock参数是从下往上、从里往外传的。如果同时patch了好几个容易混。比如mock.patch(src.user_service.requests.get) mock.patch(src.user_service.logger) def test_something(self, mock_logger, mock_get): ...离被装饰方法最近的mock.patch对应的是最靠近方法的那个参数也就是mock_get在最后、mock_logger在前。如果记不住可以在方法里打个print(mock_get)看看是不是你想象的那个对象。第三种只mock了返回值却没有设置side_effect。return_value是固定返回值side_effect可以是异常、可迭代对象或函数。如果你要模拟第一次调用成功、第二次调用抛异常用side_effect[response, exception]就很自然mock_get.side_effect [mock_response, requests.Timeout]弄清楚上面这三个错法Mock基本就不会再折磨你了。6. 面向真实项目的进阶覆盖率、异步与CI6.1 覆盖率工具只看数字也会骗人测试写得多了团队就会关心测了百分之多少。Python生态里最常用的覆盖率工具是coverage配合unittest可以这样跑coverage run -m unittest discover -s tests coverage report -mcoverage report -m会列出每个文件的覆盖率百分比以及哪些行没有执行到。常见输出长这样Name Stmts Miss Cover Missing ----------------------------------------------- src/order.py 20 2 90% 45-46 src/user_service.py 15 3 80% 22-24单看百分比90%看起来不错。但覆盖率只是一个参考它只代表哪些行被执行了不代表所有逻辑都被验证了。一个函数哪怕每行都执行到了也可能因为断言不够充分而存在逻辑bug。比如你调用了calculate_discount(100, gold, False)返回了90你只断言它不是None那这个断言几乎没有价值但覆盖率仍然是100%。所以我的建议是覆盖率数字看可以但更要看的是关键分支是否被覆盖。在一个if...else里只覆盖了if分支代码行数可能都执行了但else分支还是空白。这时候我一般会手动把未覆盖的行数拎出来逐个确认是否需要补用例。覆盖率工具是帮你找盲区的不是用来刷KPI的。6.2 为asyncio协程写测试unittest也能测异步很多Python项目现在都用了asyncio协程函数不能直接assertEqual(await foo(), 1)因为测试方法本身不是异步的。unittest在Python 3.8之后引入了IsolatedAsyncioTestCase专门用来测试异步代码import asyncio import unittest async def fetch_score(user_id): await asyncio.sleep(0.01) return user_id * 2 class TestAsync(unittest.IsolatedAsyncioTestCase): async def test_fetch_score(self): result await fetch_score(21) self.assertEqual(result, 42)IsolatedAsyncioTestCase会为每个异步测试方法创建独立的事件循环setUp和tearDown也可以写成异步的class TestAsync(unittest.IsolatedAsyncioTestCase): async def asyncSetUp(self): self.data await load_fixture() async def asyncTearDown(self): await cleanup(self.data)如果你还在用老写法在unittest.TestCase里写asyncio.run(...)去包住协程也能凑合但一旦协程内部有多个并发任务或者依赖事件循环的状态很容易出现事件循环已关闭的报错。直接用IsolatedAsyncioTestCase就省心很多。另外异步代码中如果有未等待的Task测试结束时会收到Task was destroyed but it is pending警告。这种问题很隐蔽建议在asyncTearDown里显式取消所有任务比如保存一个self.tasks列表然后逐个cancel。6.3 把测试跑进CI提交前先自动化单元测试如果只在本地跑它的约束力是很弱的。人总有偷懒的时候今天少跑一次明天少跑一次bug就又溜进来了。我的做法是把它接进CI流程让每次提交都自动跑。在GitHub或GitLab上可以用一个简单的workflow文件来跑。比如GitHub Actions的基础配置长这样name: run-tests on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-pythonv5 with: python-version: 3.12 - run: | python -m venv venv source venv/bin/activate pip install -r requirements.txt - run: | source venv/bin/activate python -m unittest discover -s tests -v这个配置没有用到任何第三方服务就是标准的python -m unittest discover。我把coverage的执行也加进去但不会强制覆盖率低于多少就失败。我的经验是强制覆盖率目标很容易诱发为了凑数字而写的无用断言。相比之下我更看重每次代码变更都保证测试全绿以及关键逻辑必须有对应的测试用例这两件事。把测试放入CI还有一个额外好处新人接手项目时只要看一眼CI配置就知道这个项目有测试提交前必须跑绿这种工程文化的传递比任何文档都有效。7. 我在实战里踩过的坑一次性告诉你7.1 测试之间互相污染共享状态最容易翻车前面提到过setUpClass的共享资源问题实际上测试污染还有更隐蔽的情况。比如某个测试不小心修改了模块级全局变量、环境变量、或者当前工作目录后面的用例就会莫名其妙挂了。我遇到过一次非常诡异的现象单独跑test_a是绿的单独跑test_b也是绿的但整个测试套件一起跑test_b就红了。排查了半天发现是test_a里某个函数调用会修改os.environ里的一个变量而test_b依赖这个环境变量的原始值。从那以后我给自己立了一条规矩用例里能用局部变量就用局部变量尽量不要依赖全局状态如果确实要改环境变量用mock.patch.dict(os.environ, {...})包裹测试结束自动恢复。比如class TestEnv(unittest.TestCase): mock.patch.dict(os.environ, {APP_ENV: testing}) def test_uses_env(self): self.assertEqual(os.environ[APP_ENV], testing)这样可以确保每个用例运行完之后os.environ恢复到原样。7.2 浮点数相等的坑不要直接用assertEqual前面提到过浮点数精度问题这里再举一个更具体的例子。假设你写了一个平均价计算函数def average_price(prices): return sum(prices) / len(prices)如果你用assertEqual(average_price([0.1, 0.2]), 0.15)去测试十有八九会失败因为0.1和0.2的二进制表示是近似值实际结果是0.30000000000000004再除以2最后可能和你预期的0.15差一点点。这种问题非常容易让新手误以为我的代码写错了其实只是断言方式不对。处理浮点断言我的建议是能用assertAlmostEqual就用assertAlmostEqual如果测试的是货币类逻辑把金额单位从元换成分用整数运算可以彻底避开浮点误差如果必须用浮点同时涉及很多运算步骤可以考虑round(actual, 2)之后再和期望值比较。7.3 测试代码的维护被测代码改了测试要跟着改写测试不是一锤子买卖它和业务代码一样需要维护。我在评审代码时经常看到这种情况业务函数改了参数旧的测试用例还在用旧参数调用一跑就红。有些人选择临时改一个无关的断言硬把测试变绿这比不写测试还糟糕因为它制造了一种测试通过的假象。正确的做法是当需求变化时先更新测试用例的描述和预期再更新被测代码。这样每一条用例都对应一个明确的行为约定测试和实现始终同步。如果某些用例描述的需求已经不存在了就该把这个用例删掉或改写成新需求而不是留在那里假装测过了。我自己的习惯是在代码评审时有一个专门的检查项改动是否新增了对应的用例如果没有原则上不通过评审。这么做可能会让写代码的速度慢一点但它换来的信心和稳定是线性增长的。单元测试这条路真正走下来你会发现最难的不是学会API而是养成为每段关键逻辑补上测试的习惯。unittest作为标准库虽然不像一些第三方框架那样花哨但足够稳定、足够可靠也足够你完成从个人脚本到中型项目的覆盖需求。希望这篇实战内容能帮你少踩一些我踩过的坑把项目的底座一点点夯结实。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门