ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python单元测试入门实战:从unittest机制到Mock与CI实践

2026/10/8 4:57:45 拓冰建站 浏览量
Python单元测试入门实战:从unittest机制到Mock与CI实践 写完一个功能你习惯怎么验证我早年是手动跑一遍脚本输入几组数据看着输出“正常”就宣布收工。结果隔了两周需求一变改动一个参数整条链路全跪了。后来我服了代码敢不敢改不在你多细心而在你有没有一套能在几秒钟内自动把旧行为全部校验一遍的东西——这就是单元测试。坦白说Python项目里写爬虫的、做数据处理和Web开发的很多人对unittest的印象还停留在“听说过、没见过”。但作为标准库自带的老牌测试框架它不需要额外安装依赖不需要配置文件天然适配各种规模的项目。这篇文章我会从框架的底层机制开始讲再用一个真实的业务模型做完整实战最后把我在项目里踩过的坑和排查方法全部整理出来。适合谁看刚入门的Python新手老项目里完全没有测试的“游击队”以及想给团队引入测试规范但不知道怎么落地的同学。前面部分我会尽量讲得大白话后面的进阶技巧也不会太水有基础的可以直接跳到实战章节。1. 单元测试究竟在解决什么问题1.1 没有测试的代码像一座危房写代码本质是不断做决策函数返回什么、异常怎么处理、边界条件卡在哪里。没有测试的时候这些决策只存在于你的脑子里换个人来改或者你自己过两个月再回来完全不知道当初“这里为什么要这么写”。我见过不止一个项目线上跑得好好的某次“顺手优化”把一个函数的入参校验删了结果下游所有调用方全炸。更气人的是爆炸原因排查了两天最后发现是半年前的一次重构没有回归验证。单元测试最大的价值不是证明代码“现在是对的”而是保证“将来还是对的”。只要跑一遍测试套件旧行为有没有被破坏几秒钟就能看出来。1.2 unittest能做什么适合谁unittest是Python标准库自带的测试框架灵感来自Java的JUnit。它把测试组织成TestCase类每个test_开头的方法就是一个独立的测试用例。框架负责自动发现测试、执行用例、汇总结果还提供了mock、测试套件、fixture等一整套机制。适合用它的人主要分三类新项目从零开始不想引入额外依赖标准库就够用。老项目里完全没有测试需要一个轻量方案先跑起来。需要在CI持续集成环境里跑测试unittest零安装零配置任何Linux服务器都能直接执行。也有人问为什么不直接学pytest。pytest确实语法更简洁插件生态更丰富。但unittest作为标准库写出来的测试代码在没有任何第三方环境的地方都能跑而且很多大型开源项目至今仍用它。理解unittest的机制再看pytest的conftest、fixture反而非常快。1.3 测试金字塔单元测试是基石软件测试领域的测试金字塔大概是这样最底层是单元测试数量最多运行最快隔离性最强中间是集成测试验证模块之间能不能正确协作顶层是端到端测试模拟真实用户操作跑得最慢也最脆弱。我发现很多团队反着来整天写端到端测试一个用例要启动数据库、拉起Redis、调用第三方接口跑一遍十分钟。这种测试一旦失败你根本不知道是前端逻辑错了、后端接口变了还是测试环境数据被污染了。正确的做法是把80%的精力放在单元测试上接口和业务流程留给集成测试端到端测试只覆盖核心链路。unittest正是单元测试这一层最趁手的工具。2. unittest核心机制不搞懂这些后面全是坑2.1 只需要一个文件夹约定Python自动找到你的测试unittest的测试发现机制很简单但很多新手就是栽在这里。它默认查找当前目录及所有子目录下以test开头命名的.py文件然后在这些文件里寻找继承自unittest.TestCase的类最后执行类中所有以test_开头的方法。所以你的测试文件必须命名为test_xxx.py比如test_cart.py、test_user.py这样才能被自动发现。测试类的名字最好用TestXxx开头测试方法必须用test_开头。这三个“必须”是约定也是硬性规则少了任何一个写好的测试都会静默地不被执行。我习惯的项目结构如下project/ ├── src/ │ └── mypackage/ │ ├── __init__.py │ └── cart.py └── tests/ └── test_cart.py被测代码放在src/mypackage/测试代码放在tests/目录。在项目根目录执行python -m unittest discover -s tests -v-s tests指定扫描目录-v输出每个用例的执行详情。如果你不想指定目录也可以直接把测试文件放在项目根目录执行python -m unittest即可自动发现。注意一定要用python -m unittest而不是直接运行python test_cart.py前者会正确处理包导入路径后者容易遇到ModuleNotFoundError。2.2 一个最小用例的完整解剖给你看一个最简单的测试长什么样import unittest def add(a, b): return a b class TestAdd(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()unittest.TestCase是每个测试类的父类所有断言方法都从它继承。每个test_方法执行时可以完全独立互不依赖。最后的unittest.main()是给“直接运行本文件”用的入口如果你习惯用python -m unittest执行这段也可以不写。执行结果会显示每个用例的点号.表示通过F表示失败E表示执行中出错。失败和错误的区别很关键断言不通过是失败说明逻辑和预期不一致代码抛出异常是错误说明被测代码本身有问题。2.3 setUp与tearDown的执行顺序写测试经常需要准备数据。如果每个用例都重复写一遍初始化代码测试会变得冗长且难以维护。unittest提供了fixture机制setUp()每个测试方法执行前调用用于准备环境。tearDown()每个测试方法执行后调用用于清理资源。setUpClass()整个测试类执行前调用一次必须是类方法。tearDownClass()整个测试类执行后调用一次必须是类方法。执行顺序是setUpClass→setUp→test_one→tearDown→setUp→test_two→tearDown→tearDownClass。这里有个常见的理解偏差setUp里创建的数据库连接、临时文件、网络请求如果在tearDown里没有关闭很容易泄漏资源。尤其是测试里访问了本地端口忘了关闭监听第二次跑测试就会报“端口被占用”。import unittest import tempfile import os class TestFileOps(unittest.TestCase): classmethod def setUpClass(cls): cls.tmp_dir tempfile.mkdtemp() classmethod def tearDownClass(cls): os.rmdir(cls.tmp_dir) def test_write_file(self): target os.path.join(self.tmp_dir, a.txt) with open(target, w) as f: f.write(hello) self.assertTrue(os.path.exists(target))注意setUpClass和tearDownClass需要加classmethod装饰器参数是cls而不是self。这个细节写错过的人非常多。2.4 常用断言速查别再用print看结果unittest最大的好处就是自带丰富的断言方法。所谓断言就是“我认定结果一定是这样如果不是测试就失败”。新手最常见的习惯是写assert关键字或者print打出来肉眼判断这在自动化测试里是灾难。assert关键字抛出的AssertionError缺少上下文信息排查问题时根本不知道实际值是什么。我整理了最常用的一组断言方法方法用途典型场景assertEqual(a, b)判断相等函数返回值是否符合预期assertNotEqual(a, b)判断不相等排除某个值assertTrue(x)判断为真布尔标志位assertFalse(x)判断为假布尔标志位assertIs(a, b)判断是同一个对象验证对象身份assertIsNone(x)判断为None空值处理assertIn(a, b)判断a在b中列表、字符串、集合成员assertNotIn(a, b)判断a不在b中排除某个元素assertRaises(异常)判断抛出指定异常参数校验、错误处理assertAlmostEqual(a, b)浮点数近似相等计算金额、概率assertGreater(a, b)判断a大于b数值范围浮点数比较一定不要用assertEqual。0.1 0.2在计算机里并不精确等于0.3直接断言相等大概率失败。遇到金额计算稳妥的做法是保留两位再比或者直接用assertAlmostEqual指定小数位。2.5 组织套件从零散文件到统一入口项目大了以后测试文件会越来越多。一个一个跑不现实这时候需要TestSuite和TestLoader。python -m unittest discover -s tests -t src -p test_*.py-p指定文件匹配模式默认就是test*.py。如果你不想让unittest自动发现那些“只是名字带test但不是测试文件”的脚本也可以在测试类里通过load_tests函数控制加载逻辑。需要细粒度控制时可以手动构建套件import unittest from tests.test_cart import TestCart from tests.test_user import TestUser def suite(): loader unittest.TestLoader() s unittest.TestSuite() s.addTests(loader.loadTestsFromTestCase(TestCart)) s.addTests(loader.loadTestsFromTestCase(TestUser)) return s if __name__ __main__: runner unittest.TextTestRunner(verbosity2) runner.run(suite())很多人写到这里就停了。其实unittest还有一个容易被忽略的loadTestsFromModule方法可以在一个文件里汇总多个模块的测试非常适合分层分模块组织回归测试。我建议小项目直接用discover就够了等测试文件超过20个再考虑手工编排套件。3. 实战给购物车业务模型编写完整测试3.1 先定业务规则再写测试光讲概念没有说服力我拿一个真实的业务场景来做完整实战。假设你在开发一个电商系统的购物车模块需求如下购物车可以添加商品每个商品有名称、单价、数量。添加时校验单价不能为负数数量必须大于0。计算商品总价单价乘以数量后累加。结算时有优惠策略总价200元以上减30300元以上减50优惠不叠加取最高档。你会发现业务规则非常清晰这其实就是测试用例最好的来源。每条规则对应一个或多个测试规则之间的边界条件是测试的核心。3.2 先写测试还是先写业务代码我习惯用“测试先行”的思路走一遍但不会教条地坚持完整TDD。最简单有效的方式是先写被测试的Cart类跑一次测试看到失败再补齐逻辑让它通过。这样既能看到真实反馈又不会被TDD流程拖慢节奏。被测模块src/mypackage/cart.py代码如下class Cart: 购物车维护商品列表并计算结算金额。 def __init__(self): self.items [] def add_item(self, name, price, quantity1): if price 0: raise ValueError(商品单价不能为负数) if quantity 0: raise ValueError(商品数量必须大于0) self.items.append({ name: name, price: price, quantity: quantity, }) def total_price(self): 所有商品原价总和。 return sum(item[price] * item[quantity] for item in self.items) def checkout_amount(self): 结算金额应用满减优惠。 total self.total_price() if total 300: return total - 50 if total 200: return total - 30 return total注意add_item的异常处理就是给assertRaises准备的。如果业务代码里没有参数校验那测试里也写不出异常断言反过来是一样写测试时发现哪里需要异常通常也意味着被测代码少了一层防护。3.3 测试代码逐行拆解测试文件tests/test_cart.py如下import unittest from mypackage.cart import Cart class TestCart(unittest.TestCase): def setUp(self): self.cart Cart() def test_empty_cart_total_is_zero(self): self.assertEqual(self.cart.total_price(), 0) self.assertEqual(self.cart.checkout_amount(), 0) def test_add_item_updates_items_list(self): self.cart.add_item(奶茶, 15, 2) self.assertEqual(len(self.cart.items), 1) self.assertEqual(self.cart.items[0][name], 奶茶) self.assertEqual(self.cart.items[0][price], 15) self.assertEqual(self.cart.items[0][quantity], 2) def test_total_price_single_item(self): self.cart.add_item(咖啡, 18, 3) self.assertEqual(self.cart.total_price(), 54) def test_total_price_multiple_items(self): self.cart.add_item(面包, 12, 2) self.cart.add_item(牛奶, 10, 3) self.assertEqual(self.cart.total_price(), 54) def test_checkout_no_discount_below_200(self): self.cart.add_item(柠檬水, 8, 10) self.assertEqual(self.cart.checkout_amount(), 80) def test_checkout_discount_30_when_200_to_299(self): self.cart.add_item(卫衣, 99, 2) self.cart.add_item(袜子, 15, 2) total self.cart.total_price() self.assertEqual(total, 228) self.assertEqual(self.cart.checkout_amount(), 198) def test_checkout_discount_50_when_over_300(self): self.cart.add_item(耳机, 299, 1) self.cart.add_item(数据线, 49, 1) total self.cart.total_price() self.assertEqual(total, 348) self.assertEqual(self.cart.checkout_amount(), 298) def test_add_item_rejects_negative_price(self): with self.assertRaises(ValueError): self.cart.add_item(测试商品, -5, 1) def test_add_item_rejects_zero_quantity(self): with self.assertRaises(ValueError): self.cart.add_item(测试商品, 10, 0) def test_checkout_discount_boundary_200(self): self.cart.add_item(凑单商品, 200, 1) self.assertEqual(self.cart.total_price(), 200) self.assertEqual(self.cart.checkout_amount(), 170) def test_checkout_discount_boundary_300(self): self.cart.add_item(凑单商品, 300, 1) self.assertEqual(self.cart.total_price(), 300) self.assertEqual(self.cart.checkout_amount(), 250) if __name__ __main__: unittest.main()每个测试方法的命名都是test_开头后面跟“行为预期结果”的描述。比如test_checkout_discount_30_when_200_to_299看名字就知道它在验证200元到299元区间是否减30。这比test_cart_3这种命名好一万倍测试失败的回报信息直接告诉你是哪个业务规则崩了。3.4 跑起来命令行执行和结果解读在项目根目录执行python -m unittest discover -s tests -v输出大致长这样test_add_item_rejects_negative_price (tests.test_cart.TestCart) ... ok test_add_item_rejects_zero_quantity (tests.test_cart.TestCart) ... ok test_add_item_updates_items_list (tests.test_cart.TestCart) ... ok test_checkout_discount_30_when_200_to_299 (tests.test_cart.TestCart) ... ok test_checkout_discount_50_when_over_300 (tests.test_cart.TestCart) ... ok test_checkout_discount_boundary_200 (tests.test_cart.TestCart) ... ok test_checkout_discount_boundary_300 (tests.test_cart.TestCart) ... ok test_empty_cart_total_is_zero (tests.test_cart.TestCart) ... ok test_total_price_multiple_items (tests.test_cart.TestCart) ... ok test_total_price_single_item (tests.test_cart.TestCart) ... ok ---------------------------------------------------------------------- Ran 10 tests in 0.002s OK一眼扫过去10个测试全部通过。这里Ran 10 tests很重要如果显示的是Ran 0 tests那大概率是测试发现机制出了问题。我后面在问题排查章节会专门展开。3.5 用覆盖率量化你的测试是否到位测试全绿不代表测试写够了。你有可能只测了正常路径异常分支全是空的。这时要用覆盖率工具来量化。pip install coverage coverage run -m unittest discover -s tests coverage reportcoverage report会输出每个文件的覆盖率包括行覆盖率和分支覆盖率。购物车这个模块如果测试写得完整覆盖率应该接近100%。但要注意覆盖率100%不等于没有bug它只能证明每一行代码都被执行过不能证明所有执行结果都被校验过。覆盖率的意义在于发现“你没测到的地方”。比如我见过一个支付模块平时跑测试覆盖率80%细看发现if retry_count 3这个重试分支根本没人执行过。后来线上真的出现重试超限那段逻辑直接报错。覆盖率数字低不可怕可怕的是你根本不知道哪里没测。4. 高级技巧mock外部依赖让测试真正跑起来4.1 什么样的代码需要mock单元测试的核心要求是“隔离”。如果被测函数发起了真实的HTTP请求、读取了当前机器的环境变量、操作了数据库那么测试结果就会受到外部环境的影响。今天网络通就过明天网络断了就挂这不是单元测试这是脆弱的集成测试。unittest.mock专门解决这个问题。它的思路很简单把被测代码里依赖的外部对象替换成我们预设的假对象测试只验证代码“是否按预期调用了外部接口以及拿到返回结果后是否做了正确的事”。典型场景包括调用第三方API获取用户信息。读取系统当前时间。随机数生成。发送邮件、短信。访问数据库或文件系统。4.2 patch和patch.object的正确用法看一个场景业务函数需要调用外部接口获得用户名再拼接成一句欢迎语。import requests def fetch_user_name(user_id): resp requests.get(fhttps://api.example.com/users/{user_id}) data resp.json() return data[name] def build_greeting(user_id): name fetch_user_name(user_id) return f你好{name}测试时绝不能真的访问www.example.com。用patch替换掉requests.getfrom unittest.mock import patch from mypackage.greeting import build_greeting class TestGreeting(unittest.TestCase): patch(mypackage.greeting.requests.get) def test_build_greeting(self, mock_get): mock_get.return_value.json.return_value {name: 张三} result build_greeting(1) self.assertEqual(result, 你好张三) mock_get.assert_called_once_with(https://api.example.com/users/1)注意patch写的路径是mypackage.greeting.requests.get而不是requests.get。因为greeting.py里通过import requests引用后这个模块的命名空间里已经有requests这个名字了。如果 patch 了全局的requests.get而被测模块里 import 方式是from requests import get那就完全失效。这个细节几乎每个用过mock的人都会踩我在项目评审里帮人排查过多次。4.3 return_value和side_effect的区别return_value决定mock对象被调用后返回什么适合稳定返回的场景。side_effect则强大得多它可以传入异常对象调用时抛错、一个函数根据参数动态返回或者一个可迭代对象每次调用返回不同的值。我最常用side_effect的两个地方第一个模拟调用失败后重试成功的场景from unittest.mock import Mock mock_api Mock() mock_api.side_effect [requests.exceptions.ConnectionError, {ok: True}] # 第一次调用抛错第二次调用返回字典 try: mock_api() except requests.exceptions.ConnectionError: pass self.assertEqual(mock_api(), {ok: True})第二个验证函数是否对异常做了正确处理patch(mypackage.greeting.fetch_user_name) def test_greet_when_api_fails(self, mock_fetch): mock_fetch.side_effect requests.exceptions.RequestException result build_greeting(666) self.assertEqual(result, 服务暂时不可用)如果被测函数没有捕获异常这个测试就会报错而不是失败报错信息会直接指出fetch_user_name抛出了未处理的异常。这对排查“异常处理逻辑到底有没有生效”非常直观。4.4 mock常见的三个误区第一个误区过度mock。把函数内部所有依赖全部替换掉之后其实测试的意义已经被掏空。比如测一个排序函数非要mock掉list.sort那就完全没意义了。判断标准是mock的应该是“外部IO或者不稳定依赖”而不是内部核心逻辑。第二个误区忘了断言调用次数。只mock返回值但不检查是否被调用、调用参数是什么等于只验证了一半。像上面的例子assert_called_once_with能确保URL是/users/1而不是某个硬编码错误地址这才是接口调用类函数的核心验证点。第三个误区mock后没有还原。如果你在测试方法里直接mock.patch(...)而不通过装饰器或上下文管理器mock在测试结束后不会自动还原会污染其他用例。我遇到过requests.get被mock后一直不恢复后面所有依赖网络请求的用例全部返回假数据排查了两小时。# 正确 patch(mypackage.greeting.requests.get) def test_build_greeting(self, mock_get): ... # 错误 def test_build_greeting(self): mock_get patch(mypackage.greeting.requests.get).start() ... # 忘了 mock_get.stop()5. 测试命名、组织与团队协作规范5.1 测试文件、类和方法的命名规范测试命名不是小事它直接影响排错效率。坏名字只告诉你“测试挂了”好名字告诉你“哪个行为被破坏了”。我建议的规范文件test_被测模块.py例如test_cart.py、test_price.py。类Test被测类名例如TestCart。方法test_行为描述_预期结果例如test_total_price_multiple_items。如果说自己有一套团队规范比我这套更细也不是不行关键是全组统一。最怕的是一半人写test_a、test_b一半人写test_normal、test_error测试文件变成猜谜游戏。5.2 不要依赖测试执行顺序unittest默认按照类名和方法名的字母序执行但这是实现细节官方文档从未承诺这个顺序在将来版本保持不变。更重要的是测试用例之间不应该有隐藏的依赖关系用例A创建了一个用户用例B假设这个用户已经存在。这种依赖会让测试变得极其脆弱——单独执行B必然失败执行整个套件时也不稳定因为A不一定排在B前面。正确的做法是每个用例独立准备它需要的数据。如果准备数据的成本很高也至少要在setUp里明确创建而不是依赖另一个用例的副作用。5.3 测试数据和fixture的治理测试数据是另一个容易失控的点。我见过有人把几百行测试数据直接写在测试文件顶部各种魔数满天飞。一旦业务规则调整改数据改到怀疑人生。比较好的做法是小范围数据尽量用辅助函数生成复杂数据可以放在独立的fixtures.py里集中管理。另外一个很有用的思路是在setUp里构造对象而不是在测试方法里重复代码。像购物车例子里每个用例都创建Cart实例放setUp里就非常合适。不推荐把测试数据放到外部Excel或JSON文件里除非数据量巨大且需要业务人员维护。文件一多路径问题、编码问题、同步问题全部冒出来对单元测试来说得不偿失。5.4 把测试接入CI才有真正的敬畏测试写得再漂亮如果只在本地跑意义会打折扣。实际上让测试产生敬畏感的时刻是在提交代码后发现CI服务器上某个用例红了而你完全不知道是哪次提交导致的时候。接入CI本质上很简单。以GitHub Actions为例只需要在.github/workflows/test.yml里写一个基本流程name: Run Tests on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - uses: actions/setup-pythonv4 with: python-version: 3.11 - run: python -m unittest discover -s tests -v当然这不是唯一方案任何GitLab CI、Jenkins、或者轻量的Shell脚本都行。核心是让“跑测试”成为提交的必经环节。我在带团队时定的铁律是测试不过不准合代码这个是底线。6. 常见问题排查实录与避坑清单6.1 测试跑起来显示Ran 0 tests这个问题的概率比你想的高得多。原因通常有三个文件名不是test_开头。测试类没有继承unittest.TestCase或者类名开头不是Test。测试方法没有用test_开头。前两个很隐蔽因为文件能正常导入Python不会报错只是静默地不执行。我排查过最邪门的一个案例测试类名是CartTestCase方法名也都是test_开头但类没有继承TestCase所有用例全部跳过CI还是绿的。所以跑完测试一定要看Ran 几十 tests这个数字而不是只看最后有没有写OK。6.2 assertEqual实际值没有输出新手经常说“我没写自定义消息失败时怎么不打印结果”。实际上unittest的assertEqual在失败时通常会自动输出两边的值但前提是两边对象的__repr__方法写得足够友好。如果你比较的是自定义对象比如一个Cart实例repr默认输出Cart object at 0x...这显然没有帮助。解决办法有两个在测试失败信息里要求更精确或者给业务对象实现一个可读的__repr__。我倾向于后者让业务类自身可读性更好。def __repr__(self): return fCart(items{self.items})6.3 mock了但没生效排查步骤是什么这是第二高发的坑。你按文章里的写法patch了但测试依然发出了真实请求。先不要慌按顺序排查检查patch的目标路径是不是被测模块里的引用位置而不是定义位置。检查被测代码里是不是用了from xxx import yyy如果是patch的目标应该是xxx.yyy所在的模块。打断点在mock对象上看它到底有没有被调用。我遇到过一个很刁钻的情况被测函数里用了import requests而我patch的是requests.get按理说应该生效。但被测模块顶部还有一个全局变量缓存了get requests.get函数里直接调用的是get()。这种情况下patchrequests.get根本不会影响那个全局缓存引用。唯一的解法是找到get定义的模块去patch那个名字。6.4 测试之间互相污染单个测试全过整套测试时随机失败这是最让人抓狂的问题。常见污染源setUpClass里创建了共享对象某个用例改了它的属性其他用例就跟着受影响。mock没有还原污染了后续用例。全局变量被某个用例修改后没有恢复。外部系统有状态比如数据库、Redis、文件系统。排查思路也相对固定先用python -m unittest tests.test_xxx单独跑某个文件再用python -m unittest tests.test_xxx.TestClass.test_method精确跑到某个用例。如果单独运行全过一起跑就挂基本可以确认是状态污染。我的预防方案是三句话优先在setUp里创建数据而不是setUpClassmock一定用装饰器或with语句不要测试里改全局变量。6.5 我在真实项目里看到的测试坏味道写到最后把我给团队做代码评审时经常喷的几个问题整理出来当一份避坑清单送给你。测试方法过长一个用例里塞了十几个断言失败时完全不知道是哪一步出了问题。测试中使用了真实睡眠等待time.sleep企图等异步任务完成这是最脆弱的写法。只测正常路径不测异常路径。业务代码里的try-except分支几乎没有任何覆盖。为了覆盖率数字硬凑测试比如执行一个函数但不校验返回值。测试代码里复制了业务代码的实现逻辑然后断言两者相等这种测试等于白写逻辑错了测试也跟着错。写在最后先跑起来再追求完美我第一次给项目补单元测试时心里也是抗拒的觉得业务需求那么紧哪来的时间写这些“额外代码”。但一次线上事故让我彻底转了性那次因为改了一个金额计算的口径导致对账单全错线上数据修了整整两天。从那以后凡是涉及金额、状态流转、权限判断的逻辑我宁可加班也要先把测试写了。如果你现在还在犹豫从哪开始我的建议很简单找项目里最容易出bug、最关键的一个函数给它写十个用例跑起来感受一下测试通过时那种“这代码我敢动了”的底气。然后再慢慢扩大范围。没人规定你第一天就要把所有代码都测了单元测试是一场长跑跑起来就是赢。最后再分享一个我坚持很多年的习惯——任何时候写完一个功能先花几分钟想想“这个功能如果出问题最可能是哪几种情况”然后把它们写成测试。你写下的不是代码是给自己和后来者的一份保险。