2026/10/21 18:13:02

Python单元测试入门到进阶:pytest、fixture、mock与覆盖率实战

Python单元测试入门到进阶:pytest、fixture、mock与覆盖率实战 写下这个标题的时候我想起很多刚写完业务代码的开发者对我说过的同一句话“逻辑又不复杂为什么要写测试”这话放到两年前我也是同意的。直到有次改了一个看似无关紧要的配置项结果把另一个模块的默认行为带偏整整一个下午都在排查线上告警我才真正理解单元测试对于“可维护”“可演进”的代码有多重要。它不只是一堆 assert也不只是给领导看的覆盖率报告而是一套能帮你兜底、帮后来者理解意图、逼着你写出低耦合代码的工程防线。这篇内容面向有 Python 基础、正在往进阶迈步的开发者重点讲清楚单元测试的常见工具、写法取舍以及我从实际项目中踩过的坑和小技巧希望对想补上这一课的人有直接帮助。1. 为什么进阶第一步要补单元测试这堂课1.1 单元测试到底在测什么单元测试这个词听起来高大上本质上却非常朴素它拿一个函数、一个方法或者一个类当成最小单位喂给它一组输入然后断言它返回的结果是否符合预期。就像你在流水线上给每个零件配了一个检验工位零件合格才放过去不合格则亮红灯。放在代码里这个检验工位就是一个个测试函数红灯就是一个失败的断言。但进阶的视角会稍有不同。单元测试不只是验证“现在对不对”而是在把一个行为的规则固定下来。比如一个日期格式化函数它应该接受什么格式、非法输入怎么处理、边界条件是什么这些一旦被测试用例描述清楚就等于给未来的修改立了契约。之后任何人改动这个函数只要跑一遍测试就能立刻知道有没有违反契约。我见过不少开发者把单元测试当成额外负担实际上它更像一套自动化的“行为需求文档”而且这份文档不会说谎。它不会抱怨你文档写得太长也不会因为情绪影响判断只会在约定被破坏时精准地站出来。1.2 它真正解决的四类问题第一类是回归保护。这是单元测试最直接的收益你在老系统里加了新功能或者重构了一个内部函数担心影响其他模块只要测试覆盖到位一条命令就能把隐患暴露出来。没有测试的时候只能靠“人肉回归”点遍所有入口既慢又容易漏。很多项目后来不敢动旧代码就是没有一个安全网而单元测试就是那张网。第二类是代码可读性。一个写好的测试用例本身就是最好的调用示例它展示了函数怎么构造参数、返回什么结构、异常怎么抛出。新成员接手项目时与其翻一遍业务代码不如先读测试理解速度会快很多。你会发现测试写得清楚的项目团队成员之间的沟通成本也会低一些因为很多“这个函数到底怎么用”的疑问打开测试文件就有答案。第三类是设计反馈。经常有人问我为什么自己写的代码总是测不动因为被测代码往往耦合了太多东西全局变量、直接访问数据库、把网络请求写在业务逻辑里。当你想为它写测试时那些别扭就会变成一道道障碍逼着你去拆分职责、显式传参。这种“被测试逼出来的设计改进”价值远超测试本身。换句话说写测试的过程其实是一次免费的代码评审它会逼你用更清晰的接口来表达意图。第四类是调试成本控制。有了测试定位 bug 的速度会快很多。你不需要通过日志一层层猜只需要跑定向用例、看失败断言很快就能确定是业务逻辑错了还是数据没对齐。尤其是那种“改一处崩一片”的老项目一条失败的测试能直接告诉你崩在什么地方而不是让你在前端、后端、数据库之间来回翻线。这些内容听起来像理念其实都体现在一行行代码里。接下来我先把工具选型讲清楚再进入具体写法。2. 工具选型unittest 还是 pytest2.1 unittest经典但略显笨重unittest 是 Python 标准库自带的测试框架最大的优点就是不用安装第三方包环境再干净也能直接跑。它的核心模型和 JUnit 一脉相承用类和方法组织用例支持 setUp、tearDown 这种前后置逻辑还自带了一批断言方法比如 assertEqual、assertTrue、assertRaises。对于只求快速验证的小脚本unittest 完全可以胜任。但它的别扭之处也很明显。以我自己的感受来说写一组简单的数据校验测试用 unittest 要定义一个 TestCase 子类、实现 test 开头的方法还要注意继承关系如果稍微涉及公共初始化就得考虑 setUp 这些钩子。代码看起来会有不少模板感可读性稍差。而且 unittest 的断言方式跟 Python 原生的 assert 不同你得记它的方法名有时候写起来总觉得绕。当然这不代表 unittest 就没有价值。很多历史项目、系统自带包、某些工具链仍然以 unittest 为基础。即便你打算全面采用新框架也需要认得 unittest 的写法因为接手老项目时大概率会撞上它。我有一个原则标准库的能力可以不用但不能不认得尤其是在没有网络环境、装不了第三方包的部署环境里unittest 是你最后的兜底方案。2.2 pytest为什么它成了事实标准pytest 的核心优势是全方位的“轻”。它没有强制类结构普通的函数就可以当测试用例交给它时只要函数名以 test_ 开头就能自动收集。断言直接用 Python 原生的 assert失败时 pytest 会通过差异对比告诉你哪两个值不一致信息更直观。刚上手的人不需要理解什么框架哲学写一个函数、写一个 assert就能跑起来。真正让它拉开差距的是两个特性fixture 和参数化。fixture 是一个灵活的依赖注入机制可以非常优雅地处理测试前的数据准备和环境搭建参数化则让你用极少的代码覆盖多组输入输出。再加上体量庞大的插件生态比如测试报告、覆盖率、超时控制、重跑失败用例等都是插拔即用。论上手难度pytest 比 unittest 更低论进阶能力它也经得起重度使用。对比项unittestpytest用例组织类和方法普通函数即可断言方式专用断言方法原生 assert带差异提示前置/清理setUp / tearDownfixture支持依赖注入参数化需要 subTest 或手工循环内置 parametrize插件生态较少丰富学习曲线中等平缓这个表不是我硬凑的而是实际写过两种框架后的直观感受。pytest 的插件机制还有一个好处你不需要把各种能力内置到框架里按需加插件就行。这让 pytest 本身保持轻量也让团队可以按自己的工程规范组装测试工具链。2.3 选型建议别纠结看场景给一个简单可操作的结论如果是新项目尤其是你想把测试作为日常习惯来培养直接用 pytest能够降低写测试的心理门槛生态也更好。如果是维护老项目老项目用什么你就跟着用什么没必要强行引入新框架。很多时候纯标准库环境不允许加包unittest 也一样能完成工作。有一点要注意无论选哪个测试框架只是载体核心是测试思维。别陷入框架之争而忘了正事。下面我就以 pytest 为主线把整个测试搭建过程从头走一遍。你会发现很多思路在 unittest 里也能迁移只是语法稍有不同。3. 从第一个测试用例开始断言是骨架3.1 一个最小可跑的测试文件先拿最常见的计算函数举例。假设我写了一个除法函数# core_math.py def divide(a: float, b: float) - float: if b 0: raise ValueError(除数不能为0) return a / b同一目录下建一个 test_core_math.pyimport pytest from core_math import divide def test_divide_normal(): assert divide(10, 2) 5 def test_divide_zero_raises(): with pytest.raises(ValueError): divide(10, 0)然后在命令行里执行pytest -q跑完会看到两个点表示两个用例通过。如果你只有一个测试文件也可以执行pytest test_core_math.py -q精确指定。这些基础命令很简单但建议把它们固化在一个命令里pytest -q --disable-warnings因为项目大了之后pytest 可能会输出版本不匹配的警告默认不显示有时候反而更清爽。需要特别提醒测试文件名一定以 test_ 开头或以下划线 _test.py 结尾pytest 的默认收集规则只认这种模式。如果你起名不规范即使写了用例也跑不到这是新手最容易踩的第一个坑。另外测试文件最好和被测试模块保持清晰的路径关系比如 tests 目录放测试src 目录放业务代码再用 pytest.ini 或 pyproject.toml 指定 testpaths以免收集范围过大。3.2 断言方式与常见陷阱断言是整个测试最重要的部分。pytest 里直接用 assert 表达式失败时会显示比较结果。比如def test_dict_compare(): user {name: A同学, age: 20, active: True} assert user[age] 18 assert user.get(email) is None浮点数比较是高频坑。由于二进制浮点误差0.1 0.2 0.3在多数语言里都是 FalsePython 也不例外。正确做法是用pytest.approxdef test_float(): assert (0.1 0.2) pytest.approx(0.3)异常测试用pytest.raises它不仅可以断言抛异常还能捕获异常内容with pytest.raises(ValueError) as excinfo: divide(1, 0) assert 除数 in str(excinfo.value)这个写法比try/except更好因为try/except会吞掉异常如果函数没有按预期抛错你还要额外加else分支才能发现问题而pytest.raises在异常没发生时直接让用例失败语义更清晰。我还见过有人用assertRaises但没写上下文管理器导致断言根本没生效这种隐蔽问题全靠代码审查才能发现。还有一个容易忽略的点断言尽量不要只写assert True或不带条件。我们看过一些测试里面只有一段打印和一个恒真断言等于白测。测试的价值在于用“明确的预期”去对抗“未知的实现”如果没有预期跑一万次也发现不了问题。哪怕只是检查函数返回了非空结果也比什么都不校验强。断言的粒度可以细一点宁可多写几个 assert也不要一个大杂烩断言盖住所有细节。4. 进阶核心用 fixture 管理环境和数据4.1 为什么非要用 fixture 不可很多测试需要前置条件测试连数据库要建临时表、测试读文件要准备临时目录、测试调 API 要伪造返回值。如果这些前置逻辑散落在每个用例里代码会立刻变得又长又重复。unittest 时代的解法是 setUp 和 tearDown但问题在于它们只能按类组织不能按需复用一旦多个测试类需要同样准备逻辑就得写两遍。pytest 的 fixture 改变这个局面它是一段可以被声明式使用的“准备逻辑”测试函数只需要把 fixture 名当作参数放进去pytest 就会自动调用它并注入结果。更强大的是 fixture 可以互相嵌套、有作用域控制、还能定义清理逻辑形成一个清晰的依赖图。用生活里的例子来说fixture 就像餐厅的后厨备菜提前把菜洗好切好客人点菜时直接下锅效率高也不会每个厨师各备各的。4.2 用 fixture 搭一个带清理的环境举个例子。假设被测函数依赖一个临时目录需要在里面写入配置再读取。fixture 可以这么写import pytest pytest.fixture def temp_dir(tmp_path): config_file tmp_path / config.ini config_file.write_text([default]\ntimeout5\n, encodingutf-8) yield tmp_path # 这里可以写清理逻辑pytest 会在用例结束后执行测试函数用起来def test_load_config(temp_dir): cfg load_config(temp_dir) assert cfg.timeout 5tmp_path 是 pytest 内置 fixture专门提供测试专用的临时目录用例结束自动清空不用自己管理路径。上面例子说明 fixture 的返回值可以直接注入测试需要清理时用 yield 把它拆成两段yield 之前是准备yield 之后是清理。有些场景需要显式关闭数据库连接、删除临时资源就可以放在 yield 后面。这种写法的好处是清理逻辑跟准备逻辑放在一起不会出现“setUp 里建了资源tearDown 里忘了关”的错位。你可能好奇为什么不用return tmp_path然后让系统自己回收。确实很多临时目录可以让系统自动回收但如果你的测试会打开文件句柄、创建子进程、写入中间产物不及时清理会在长时间跑测试时积累垃圾甚至触发跨用例干扰。所以yield 清理不是可有可无的仪式感而是必要的卫生习惯。4.3 fixture 的作用域和共享策略fixture 默认是函数级每个测试函数都会执行一次。如果准备逻辑很重比如启动一个测试数据库或加载模型可以给它指定作用域pytest.fixture(scopemodule) def db_conn(): conn create_db() yield conn conn.close()scope 可以取 function、class、module、session。scope 越大执行次数越少但状态污染风险也越大。比如 module 级 fixture 返回一个会被修改的列表前一个用例改了列表内容后一个用例拿到的就是被改过的数据。我建议默认用 function 级只有确实重量级的资源再提升 scope并且要保证 fixture 内部是“只读”或“每次重置”的。另外多个测试文件都要用到的 fixture 可以放在根目录的 conftest.py 中。pytest 会自动识别 conftest.py 里定义的 fixture不需要 import。如果你只在某个模块里定义 fixture其他模块想用是不行的这在项目分层后会是一个很常见的困惑。conftest.py 是共享 fixture 的集中营但别把太多业务逻辑塞进去否则维护起来也头疼。我更倾向只放跨文件的通用准备逻辑把业务相关的 fixture 放在对应测试模块里这样每个文件的职责一目了然。5. 数据驱动测试参数化解决重复劳动5.1 多组输入反复测怎么写才不累测试函数的套路经常是“给定输入断言输出”。如果同一个逻辑需要覆盖十组输入最朴素的写法就是复制十次测试函数只是参数不同。代码看起来很不专业也难维护改了一处期望值要改十处。更麻烦的是如果只改了其中一组参数其他九个容易忘记同步测试之间就出现不一致。参数化让一个测试函数自动展开成多个场景。pytest 的parametrize装饰器可以给函数注入多组参数每条参数组合对应一个测试用例失败时能清楚定位到是哪个参数组挂了。这有点像一个模板批量生成订单文案框架一样填充的数据不一样。5.2 parametrize 的使用示例沿用除法例子。我们想检查多组正常除法结果import pytest from core_math import divide pytest.mark.parametrize(a,b,expected, [ (10, 2, 5), (9, 3, 3), (7, 2, 3.5), (-6, 2, -3), ]) def test_divide_with_params(a, b, expected): assert divide(a, b) expected跑一下会看到 4 个用例名字会自动带上参数标记比如test_divide_with_params[10-2-5]。如果某个场景失败pytest 的 -v 输出里会显示具体参数方便直接定位。实测下来参数化测试最直观的好处就是“一个函数能当多个用例用”维护时只改一处。参数还可以配合 fixture 使用。比如 fixture 返回一个环境对象参数化注入不同的配置字符串pytest.fixture def env(tmp_path, request): name request.param path tmp_path / name path.mkdir() return path pytest.mark.parametrize(env, [dev, prod], indirectTrue) def test_env_config(env): # env 此时指向 dev 或 prod 目录 ...indirectTrue 的意思是让参数名直接作为 fixture 名解析这个技巧进阶用得比较多初学可以先了解。它可以把“环境差异”变成测试矩阵的一维让同一套断言跑在不同配置上非常有价值。5.3 参数化时应该覆盖哪些边界不要只写“正常场景”。我建议每组参数化至少考虑最常规的取值、边界值最小值和最大值附近、空值或 None、异常输入。还是以除法为例除数接近 0 但不等于 0 的情况、除数为 0 的情况、被除数为非常大的数的情况都是值得覆盖的。写参数化之前先想想这个函数的自然边界在哪里。数据驱动的价值不在于数量多而在于覆盖面广。这里有一个反模式把多个无关的行为塞进同一个参数化列表导致测试失败时你还要判断是哪个语义出问题。所以参数化最好按“同一个业务规则下的不同数据”来组织不要把所有断言都揉在一起。比如“除法计算结果”和“除数为零的错误提示”是两个行为应该拆成两个参数化函数否则一条数据失败时定位问题的时间会变长。6. 隔离外部依赖mock 让测试又快又稳6.1 为什么要隔离外部依赖真实项目基本都有外部调用HTTP 请求、数据库读写、消息队列、第三方 SDK。如果测试直接连外部系统会遇到三类麻烦速度慢、不稳定、不可控。一次网络超时就可能让整个测试失败而这跟你的代码逻辑无关。很多开发者初学测试时都会遇到“明明没改代码测试就是红”的情况十有八九是外部依赖不稳定造成的。mock 的核心思想是用一个模拟对象替换真实外部依赖让被测代码认为它在和真实系统交互实际上返回的是我们预设的结果。这样测试变得确定、快速也更安全。尤其在做一些涉及支付、发信、推送这类服务时不 mock 一下就跑测试既危险又费时。但注意mock 不是回避责任的工具它针对的是“与本用例无关的外部依赖”。如果你 mock 掉了被测函数本身那这个测试就没有任何意义。后面我会专门说这个度怎么把握。6.2 用 pytest-mock 模拟一个外部 API 调用先看一个待测函数import requests def fetch_user_name(user_id: int) - str: resp requests.get(fhttps://api.example.com/users/{user_id}) data resp.json() return data[name]测试时我们不能真的访问外部接口。用 pytest-mock 插件实测下来很好用def test_fetch_user_name(mocker): mock_resp mocker.Mock() mock_resp.json.return_value {name: 小A} mocker.patch(requests.get, return_valuemock_resp) assert fetch_user_name(1) 小Amocker.patch 会临时替换requests.get测试结束后自动还原。mock_resp.json.return_value 设定了 json() 的返回值于是 fetch_user_name 内部拿到 data[name] 就是“小A”。整个测试完全不需要联网速度飞快。如果你不想依赖 pytest-mockunittest.mock 里同样有 patch 装饰器和上下文管理器写法类似只是换汤不换药。pytest-mock 的好处是提供了 mocker fixture不需要手动处理 patch 的恢复逻辑更省心。而且mocker.Mock()是 unittest.mock 里的 Mock 类的别名用法一致学习成本很低。6.3 mock 的副作用和调用校验有些场景不是只返回一个值而是第一次抛异常、第二次返回正常。可以用 side_effectdef test_retry_logic(mocker): real_get requests.get call_count 0 def fake_get(url, **kwargs): nonlocal call_count call_count 1 if call_count 1: raise requests.ConnectionError(timeout) return mocker.Mock(jsonlambda: {name: 小B}) mocker.patch(requests.get, side_effectfake_get) assert fetch_user_name(1) 小Bside_effect 可以是异常、列表或函数它决定 mock 被调用时的行为。列表会按顺序返回值函数则可以精确控制更复杂的逻辑。这个能力在测试重试逻辑、失败降级策略时特别重要。另外还可以断言 mock 是否被正确调用def test_fetch_called_with(mocker): mocker.patch(requests.get, return_valuemocker.Mock(jsonlambda: {name: x})) fetch_user_name(42) requests.get.assert_called_once_with(https://api.example.com/users/42)这里的灵魂是“调用到了正确的 URL、正确的参数”。很多 mock 测试只关心返回值却忘了校验调用参数结果 mock 掩盖了参数拼接错误测试照样变绿。所以测试里一定要花几行来断言“外部依赖确实按照预期被调用了”。再说一下 mock 的度。网上观点很多我的经验是mock 边界要放在项目代码和外部系统的交界处。项目内部的工具函数、类之间协作尽量用真实对象只有跨到边界、面向不可控依赖时再 mock。这样测试能覆盖到我们真正写的逻辑而不是把内部实现也架空。mock 太狠测试可能绿得很虚假。比如一个内部函数明明是计算逻辑如果你 mock 掉它的入参和返回值那这个测试就只能测到一层壳没有任何价值。7. 覆盖率与测试反馈闭环7.1 覆盖率不是唯一指标但它是体检表覆盖率coverage指的是测试运行时多少比例的代码行被执行到。它不能证明逻辑是否正确但能暴露“哪些代码从来没被运行过”。对于维护过一段旧代码又没测试的人覆盖率报表是很吓人的你会看到大量分支标红说明它们已经很久没有接受过任何验证。Python 生态里最常用的是 coverage 工具也可以直接用 pytest-cov 插件。安装插件后运行pytest --covcore_math --cov-reportterm-missing输出里会列出每个文件的语句覆盖率、分支覆盖率和未覆盖行号。如果某些行长期飘红要么是死代码要么是你漏测了关键分支。这两种都需要处理但力度不同。死代码可以直接删漏测分支则要想办法补用例。7.2 如何把覆盖率真正用起来覆盖率不应该成为一个“百分比游戏”。不同模块的合理覆盖率差异很大核心业务函数、解析器、对外接口应该尽量高别人看起来像脚本的一些 CLI 入口、调试代码可以适当放宽。建议的做法是在核心模块设定一个最低阈值比如--cov-fail-under80低于 80 就让测试失败在 CI 里强制检查。但别把阈值一次性定到 100那样会逼出大量只为了“覆盖”而写的空测试反而没有意义。另一个关键习惯每写完一个测试用覆盖率报告看一眼自己到底有没有覆盖到新写的分支。没有覆盖到的行往往就是边界条件和异常分支。我见过很多开发者写测试时喜欢挑顺路径写异常分支根本不覆盖结果上线最怕的就是异常分支出错。覆盖率报告就像体检报告你可以不追求满分但至少要知道哪些指标常年偏低。7.3 把测试接入日常流程这里说的日常流程不一定是复杂的 CI 系统。最简单的方式是给项目加一个 Makefile 或脚本把install,test,coverage等命令固化。以后每次提交前跑一遍不绿就不推。如果用了别的协作工具也可以在提交钩子里加一条命令。CI 里跑测试时建议不要只跑一次还可以考虑分环境、分测试层级。单元测试、集成测试放在不同目录用不同标记或路径来区分。我本人在做稍大一点项目时都会在 pytest 里配置[pytest] testpaths tests addopts -q --disable-warnings --maxfail5addopts 里的--maxfail5很有用一旦连续失败 5 个用例测试就停止避免一堆错误信息刷屏省时间。这也算是一个小技巧吧。如果你的项目还在成长阶段先不用急着搭完整流水线只要保证本地一条命令能跑全部测试就已经成功了一半后续再逐步加入覆盖率阈值和并行执行等项。8. 常见问题与排查技巧实录8.1 测试老是收集不到用例为什么最基础的原因有三个文件名不以 test_ 开头、函数名不以 test_ 开头、测试文件不在 pytest 的搜索范围里。pytest 默认搜索当前目录下所有 test_*.py 或 *_test.py 文件。如果你把用例放在src/底下且没有加__init__.py或配置文件也可能收不到。解决办法是先用pytest --collect-only -q看一下它到底收集了哪些用例这个命令非常适合排查。8.2 fixture 作用域导致的状态污染曾经有个项目两个测试函数共用一个 module 级 fixturefixture 返回一个列表第一个用例往里面 append 了一条数据第二个用例拿到的列表已经包含那条数据导致断言失败。这种问题很隐蔽因为单独跑第二个用例又是通过的。排查时可以先加--setup-show看 fixture 的执行顺序再思考共享数据是否被修改。最直接的解药是除非有必要否则 fixture 只用 function 级并且所有 fixture 返回的对象尽量不携带可变状态。8.3 mock 没有生效或 patch 了错误目标mock 最常见的问题是 patch 错了地方。比如你看到代码里import requests然后在测试里mocker.patch(requests.get)但没有考虑到被测模块可能写的是from requests import get。如果你 patch 了全局的 requests.get而被测代码用的是导入本地的 getpatch 就不会生效。正确做法是 patch 被测模块的命名空间比如mocker.patch(core_math.requests.get)这里的 core_math 是使用 requests 的模块名。这个错误非常经典很多初学者会困惑为什么测试还是发起了真实请求。我的经验是调用 patch 时路径名要指到“被测模块里的那个名字”而不是指到第三方库的原始位置。这也算是一个入门期最容易被绕晕的细节。8.4 测试之间互相影响跑单个通过、跑全量失败除了 fixture 状态污染还有可能是测试文件之间的全局变量修改、环境变量修改、当前目录切换等。排查时可以先缩小范围用pytest tests/test_a.py tests/test_b.py逐个组合找到哪两个文件放到一起会互踩。然后检查是否有共享的临时文件、是否有模块级变量在 import 时被改变。必要时用monkeypatch.setenv等 fixture 来隔离环境变量不要直接改 os.environ因为改了以后不恢复会影响后面用例。我遇到过最离谱的一次是某个模块通过os.chdir()切换了当前目录后面所有依赖相对路径的测试全部失败。从那以后我在测试代码里都会特别留意“有没有改变全局状态”的操作能不用就尽量不用如果非用不可必须放到 fixture 里并保证 teardown 恢复现场。8.5 排查工具快速速查命令/参数用途pytest --collect-only -q查看 pytest 到底收集了哪些用例pytest --setup-show显示每个 fixture 的创建与销毁流程pytest -x遇到第一个失败就停pytest --lf只跑上次失败的用例pytest -k 关键名按表达式筛选用例pytest --pdb失败后进入调试器这些命令组合起来能把排查效率拉高一截。特别是--pdb新手可能不熟悉但它比 print 调试好用得多在失败现场直接查看变量能节省大量来回打印的功夫。关于单元测试我最想强调的一点是它不是一个“写一次就结束”的产物而是一个随着代码演化不断修改的活文件。你重构一个函数测试可能也要改你新增一个分支测试就要补上新分支。如果测试本身变得陈旧、不再与业务行为一致那它不但没有价值反而会给人虚假的安全感。所以定期审视测试代码的维护成本和实际收益也是一项需要练习的能力。我个人的习惯是先写最简单的正常路径测试让它通过然后马上补异常分支、边界值每跑一次就打开覆盖率报告看一圈。长期下来心里对代码的掌控感会强很多。如果你还没有建立测试习惯不妨从今天这个除法例子开始把第一个真正属于自己的测试用例跑及格再慢慢扩大覆盖范围。这条路不需要一步到位但值得每一步都踩实。