ARTICLE DETAIL

资讯详情

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

Python测试实战指南:从单元测试到pytest到CI集成

Python测试实战指南:从单元测试到pytest到CI集成 Python测试这四个字我刚写Python那会儿是完全不放在心上的。那时候写脚本爬数据、写接口、跑定时任务上线方式极其粗暴——本地跑一遍没报错就部署顶多再手工点两下界面。直到有一次一个按日期算业务周期的接口在月底两天数据全错查了半天根因是跨月边界判断写成了直接用当天日号相减。这个错误如果当时有一个三行的单元测试根本不可能流到线上。从那以后我才认真对待测试这件事从unittest折腾到pytest从单测写到接口测试再一路做到CI自动集成踩过很多坑也沉淀了不少能直接用的东西。这篇文章就是想把整套经验从头梳理一遍适合刚入门Python不久、想正经搭建测试体系的同学也适合已经写过一些测试、但觉得维护成本高、想优化测试结构和执行效率的开发者。内容不拽术语全程从实际项目出发把原理、选型、写法和避坑都讲透。1. 为什么Python项目离不开测试1.1 我当年从不写测试到先写测试的转变很多刚接触Python的人都有个错觉Python是脚本语言代码量不大写测试是浪费时间。说实话我早期也是这么想的。当时写爬虫反爬策略一变代码就得跟着调写完逻辑能跑就算成功根本不会想到为爬虫写测试。但随着项目从几百行膨胀到几千行、几万行问题开始集中爆发改一个公共函数下游两三个模块的行为悄悄变了而你还不知道。那次线上日期事故让我彻底改了习惯。当时我在一个数据处理服务里写账期计算逻辑需求是每月1号到月底为一个账期我图省事用了当月日号和31比较。业务同学在非月底的日子测试都没问题偏偏在30号凌晨跑批所有当天数据全部算进了下个账期。这种边界问题靠人肉测试很难发现因为人下意识只测正常情况不会刻意去想如果今天是30号、31号、2月怎么办。而一个断言30号不越界的测试用例几十秒就能写完却能永远拦住这一类问题。从那之后我给自己定了一条规矩凡是涉及日期时间、金额计算、状态流转、边界判断的逻辑必须先写测试再写实现。这个习惯后来帮我省下了大量排查线上问题的时间。写测试表面上是多了工作量实际上是把调试的时间提前了、缩短了。一个测试跑5秒钟告诉你哪里错了比你在线上日志里翻半小时找原因高效太多。1.2 测试到底测什么单元测试与集成测试的分工很多人一提到测试就想到庞大复杂的测试体系容易焦虑。其实Python项目里最常用的就三层测试用一个生活例子来解释你要组装一台电脑单元测试就是测试每个配件是否合格——电源能不能通电、内存能不能被识别集成测试就是把这些配件实际装到一起看整机能不能点亮、系统能不能启动端到端测试则是模拟真实用户去使用这台电脑打开软件、浏览网页、播放视频看整体体验是否正常。对应到代码里单元测试测一个函数、一个类的方法重点在输入输出是否正确。它的特点是执行快、定位准哪里失败就说明哪个函数有问题。集成测试测模块之间、函数之间的配合比如一个视图函数调用了多个Service层方法最终返回的结果是否符合预期。它关注的是组合是否正确。端到端测试模拟用户操作从请求发起到页面渲染或接口响应跑完整链路。它最接近真实场景但速度慢、环境依赖重不适合频繁全量执行。我之前见过很多团队只写集成测试不写单元测试。结果是一个测试用例跑了5分钟失败了还得靠日志一步步猜到底哪个环节出错。单元测试的价值恰恰在于定位精确和反馈迅速。一个典型的Python服务里我建议单元测试至少占到70%以上集成测试20%左右端到端测试控制在10%以内这个配比跑起来以后最舒服。1.3 测试驱动开发到底要不要用很多人把测试驱动开发TDD说得神乎其神好像不红绿红绿循环就不专业。我的观点是别把TDD当教条当工具用。所谓红绿循环就是先写一个会失败的测试然后写最简代码让它变绿再重构优化。这个模式在写核心业务逻辑时非常有效因为你思考的出发点从我怎么实现变成了我怎么验证注意力会更早地放在需求和边界上。但也不是所有场景都适合TDD。比如写爬虫、写数据清洗脚本这类探索型代码一开始你都不知道目标页面长什么样、数据有哪些脏格式硬套TDD只会让自己卡死。我的做法是两套策略并行核心业务模块用TDD严格按照先测试后实现来探索型脚本和一次性任务就先把功能跑通再回头补关键逻辑的测试。注意测试的最终目的不是追求覆盖率数字好看而是保护你不犯错、帮你快速定位错误。不要把TDD搞成仪式感哪天你发现自己为了凑先写测试而写了一堆无意义的测试那就本末倒置了。2. 测试工具选型pytest为什么是首选2.1 unittest、pytest、nose三选一怎么选Python标准库里自带unittest零依赖就能用这也是很多教程喜欢拿它当入门工具的原因。但unittest的痛点很明显断言方法全是assertEqual、assertTrue这类长名字写起来繁琐fixture体系要靠setUp、tearDown方法作用域控制也不够灵活参数化测试的支持非常原始要自己拼子测试。nose2曾经是社区的主流选择但项目维护节奏一直不温不火插件生态也越来越跟不上。现在真正统治Python测试生态的是pytest它的设计思路和unittest完全不同——pytest的理念是用普通的assert断言用装饰器和fixture解决一切共享数据问题。你不需要记住各种assertXxx方法名写assert result expected就行失败时pytest还会自动展开比较结果告诉你两个值到底差在哪。我从unittest迁移到pytest之后最大的感受是测试代码的可读性提升了不止一个档次。同样是判断字典包含某个键值对unittest要写assertIn(name, data)和assertEqual(data[name], 张三)pytest直接就是assert data[name] 张三。字少意思还更清楚。2.2 pytest的断言到底好在哪里pytest最让人上瘾的就是断言。普通Python的assert语句失败时就抛个AssertionError什么信息都没有你还要手动加描述字符串。pytest的断言重写机制会在测试运行时自动分析断言表达式失败时给出左右两侧的实际值。比如你断言两个列表相等失败时它会标出第一个不相等的索引位置和对应的值排查效率极高。再配合pytest.approx处理浮点数比较。因为浮点数精度问题assert 0.1 0.2 0.3这种断言一定会让你怀疑人生用assert 0.1 0.2 approx(0.3)就不会被精度问题困扰。import pytest def test_list_equal(): expected {name: 张三, age: 20, tags: [python, test]} actual {name: 张三, age: 21, tags: [python, test]} assert actual expected # pytest会提示 age 字段不一致 def test_float_compare(): assert 0.1 0.2 pytest.approx(0.3)2.3 搭建一个可复用的测试工程骨架测试不是把测试文件往项目里一扔就完事良好的工程结构才能让测试长期可维护。我常用的目录结构是这样的project/ ├── src/ # 业务代码 │ └── myapp/ │ ├── __init__.py │ ├── services.py │ └── models.py ├── tests/ # 测试代码 │ ├── __init__.py │ ├── conftest.py │ ├── test_services.py │ └── test_models.py ├── pyproject.toml # pytest配置 └── requirements-dev.txt在pyproject.toml里加上pytest配置可以省掉很多启动参数[tool.pytest.ini_options] testpaths [tests] python_files [test_*.py, *_test.py] python_classes [Test*] python_functions [test_*] addopts -v --tbshort这里解释几个关键配置。testpaths告诉pytest去哪找测试文件python_files、python_classes、python_functions定义用例收集规则addopts里的-v是显示每条用例的执行结果--tbshort是让报错堆栈输出得简短一点不至于一屏刷不完。安装依赖一行命令搞定pip install pytest pytest-cov pytest-mock3. pytest核心功能实操3.1 第一个测试用例与断言技巧用pytest写测试的基本规则就三条测试文件以test_开头测试函数以test_开头测试类以Test开头。满足这三条pytest就会自动发现并执行。拿一个最简单的例子假设我们有个计算折扣价格的函数# src/myapp/pricing.py def calc_discount(price: float, is_vip: bool False) - float: 普通用户9折VIP用户8折价格不能为负数 if price 0: raise ValueError(price cannot be negative) return price * (0.8 if is_vip else 0.9)对应的测试文件# tests/test_pricing.py import pytest from myapp.pricing import calc_discount def test_normal_user_discount(): result calc_discount(100) assert result pytest.approx(90.0) def test_vip_user_discount(): result calc_discount(100, is_vipTrue) assert result pytest.approx(80.0) def test_negative_price_raises(): with pytest.raises(ValueError, matchnegative): calc_discount(-1) pytest.mark.parametrize(price,is_vip,expected, [ (0, False, 0.0), (100, False, 90.0), (100, True, 80.0), (9.9, True, 7.92), ]) def test_calc_discount_params(price, is_vip, expected): assert calc_discount(price, is_vip) pytest.approx(expected)这里面pytest.raises(ValueError, matchnegative)是pytest非常实用的断言异常功能。它不仅能断言函数抛出了指定异常类型还能用match参数做正则匹配异常消息防止抛了异常但抛错原因的假通过。边界值和异常分支测试是单元测试最容易遗漏的地方我在实际项目里吃过太多次亏。比如这个折扣函数光测正常用户9折、VIP用户8折根本不够价格是0、是负数、是小数的情况都要覆盖到。另外一个小技巧写测试时给用例命名要有语义。test_vip_user_discount一眼能看出来测的是什么场景而test_discount_1、test_discount_2这种名字后期维护时看十遍也不知道当初想干嘛。3.2 参数化测试减少重复代码如果règle类似的测试逻辑要跑多组数据最直观的做法是复制粘贴多个函数但那样维护成本太高。pytest的参数化功能(pytest.mark.parametrize)能把多组数据合并到一个函数里每组数据独立执行、独立报告。import pytest from myapp.pricing import calc_discount pytest.mark.parametrize(price,is_vip,expected, [ (0, False, 0.0), (100, False, 90.0), (100, True, 80.0), (9.9, True, 7.92), ]) def test_calc_discount_params(price, is_vip, expected): assert calc_discount(price, is_vip) pytest.approx(expected)参数化装饰器的第一个字符串参数是变量名列表逗号分隔第二个参数是具体的数据列表每个元素对应一组输入输出。这样执行时pytest会生成4个子用例哪个参数组合失败报告里会明确标出来。参数化还有一个高级用法是笛卡尔积式组合——在同一个函数上堆多个parametrize装饰器效果是各组参数之间做全组合。这在测试权限矩阵、状态机流转时非常方便。比如一个函数同时受用户角色和操作类型影响两个parametrize一叠加所有组合自动覆盖不用手写一堆嵌套循环。我在实际项目里用参数化最多的地方是接口入参校验测试、日期边界测试、配置项组合测试。它把测试数据和测试逻辑分离得很清晰后期加一个新边界条件只需要在参数列表里加一行元组测试逻辑一行都不用动。3.3 fixture测试数据和环境的懒加载管理fixture是pytest最强大也最难掌握的机制理解它需要先明白一个问题多个测试用例需要共享数据或环境准备时怎么做才优雅最笨的办法是每个测试函数内部都写一遍创建数据的逻辑比如准备数据库连接、生成临时文件、构造请求上下文。这种做法的坏处是代码冗余而且每个测试都去真实初始化一遍资源速度慢、容易互相干扰。pytest的fixture就是用来解决这个问题的。它用pytest.fixture装饰器标记一个函数该函数返回的数据会被自动注入到测试函数的同名参数里。import pytest pytest.fixture def sample_user() - dict: 创建一个测试用户数据 return {id: 1, name: 张三, email: zhangsanexample.com} def test_user_email(sample_user): assert sample_user[email].endswith(example.com)fixture的核心优势不只是注入数据而是它的作用域和生命周期管理。通过scope参数指定scopefunction每个测试函数执行前都重新调用一次适合不共享、易污染的数据。scopemodule同一个模块的文件内共享一次适合模块级只初始化一次的重资源。scopesession整个测试会话共享一次适合数据库连接、HTTP客户端等能极大提升性能。fixture还支持yield语法做清理操作。比如测试需要临时创建文件用完之后要删除测试需要连数据库跑完要关连接。yield之前是准备阶段yield之后是清理阶段pytest.fixture def temp_db(tmp_path): db_file tmp_path / test.db db create_database(str(db_file)) yield db db.close() def test_db_query(temp_db): assert temp_db.query(SELECT 1) 1这里又引出一个内置fixturetmp_path。pytest会为每个测试自动创建一个独立临时目录测试结束后自动清理。凡是用到临时文件、临时目录的测试都应该用tmp_path而不是自己写mktemp的路径这样既能保证隔离又不用担心测试产生的垃圾文件残留到项目目录。fixture还有一个容易踩坑的点fixture函数名和测试函数参数名必须完全一致pytest才能自动注入。如果你写的是def test_user(sample_user_data):但fixture叫sample_userpytest会直接报错说fixture找不到。4. mock与外部依赖隔离4.1 mock的原理让测试断网也能跑单元测试有一条重要原则测试对象应该是孤立的。可是实际代码中一个函数往往要调用外部HTTP接口、读写数据库、发消息队列测试时总不能真的去请求线上服务吧这时候就需要mock——用假对象替换真实依赖验证代码在特定环境下是否按预期执行。mock的名字容易让人产生误解它做的事情有两类一类是打桩stub用固定返回值替代真实调用让被测函数能顺利跑完另一类是验证行为断言某个方法被调用过几次、用了什么参数。这两件事在Python里都用unittest.mock这个标准库完成pytest生态里的pytest-mock插件对它做了封装提供更简洁的mockerfixture。具体原理可以这样理解mock是在运行时动态创建一个假对象这个假对象预先设定好了属性和返回值你把它替换到目标模块的命名空间里。当被测代码调用该对象时实际上调用的就是这个假对象于是外部依赖被完全隔离。4.2 常见mock场景与代码示例场景一mock HTTP请求不真正访问网络。# src/myapp/user_api.py 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]# tests/test_user_api.py def test_fetch_user_name(mocker): fake_resp mocker.Mock() fake_resp.json.return_value {name: 李四} mocker.patch(myapp.user_api.requests.get, return_valuefake_resp) result fetch_user_name(1) assert result 李四这里要特别提醒mock里的一个坑patch的路径应该是使用的路径不是定义的路径。上面代码里我们的被测函数是在myapp.user_api模块里调用了requests.get所以patch的是myapp.user_api.requests.get。如果你写成mocker.patch(requests.get)只能在requests模块自己的命名空间里生效myapp.user_api模块内部如果用的是from requests import get的方式导入那你也得patch到myapp.user_api.get。搞反这个关系是所有mock问题里最常见的很多新手一晚上排查不出原因就栽在这里。场景二mock时间依赖让测试在任何时候跑结果都一样。from datetime import datetime def get_greeting() - str: hour datetime.now().hour if hour 12: return 早上好 return 下午好测试这样写def test_get_greeting_morning(mocker): fake_now mocker.patch(myapp.greeting.datetime) fake_now.now.return_value.hour 9 assert get_greeting() 早上好场景三验证某个方法确实被调用以及调用参数。def send_notification(user, message): email_service.send(user.email, message) log_service.log(user.id, message) def test_send_notification(mocker): email_mock mocker.patch(myapp.notify.email_service) log_mock mocker.patch(myapp.notify.log_service) send_notification(user{email: ab.com}, messagehello) email_mock.send.assert_called_once_with(ab.com, hello) log_mock.log.assert_called_once()assert_called_once_with这种断言方式能验证调用参数是否正确是测试行为的核心手段。mock还有个常见的误区是mock过度。如果你的测试里十个依赖mock了八个那说明被测函数本身的问题设计可能有问题——函数职责太多依赖太细碎。mock越多测试离真实场景越远出假阳性的概率越大。我一般的原则是被测函数内部的关键业务逻辑尽量用真实实现只有外部副作用网络、IO、时间、随机数才需要mock。5. 覆盖率统计与CI集成5.1 覆盖率该怎么看别被数字绑架测试覆盖率是个老生常谈的话题。pytest生态里统计覆盖率最简单的方式是安装pytest-cov插件然后执行pytest --covmyapp --cov-reportterm-missing--covmyapp表示统计myapp包的覆盖率--cov-reportterm-missing会在控制台列出哪些行没有被覆盖到。输出类似这样Name Stmts Miss Cover Missing ---------------------------------------------------------- myapp/pricing.py 12 2 83% 24-25看到这个表有人很容易开始追求100%覆盖率。我的观点是覆盖率是参考指标不是KPI。真正有用的值是核心业务逻辑的覆盖率而不是所有代码的覆盖率。配置项、异常处理分支、UI代码、模板渲染这些代码覆盖率低一点问题不大但折扣计算、账期判断、状态机转移这类核心逻辑覆盖率低了就很危险。建议在配置里加一个底线阈值比如整体覆盖率不低于80%核心模块不低于90%。如果某次改动导致覆盖率明显下降就该反思是不是漏测了关键路径。[tool.coverage.report] fail_under 80在pyproject.toml加上面这段后覆盖率低于80%时pytest会直接以失败退出相当于在CI环节又多了一道强制检查。5.2 用GitHub Actions实现提交代码自动跑测试本地跑测试只是第一步真正让测试发挥价值的是接入持续集成CI每次代码提交、每次合并请求自动拉代码、装依赖、跑测试有失败立刻通知。这样多人协作时任何一个人的改动破坏了既有功能在合并之前就能被拦住而不是等部署到线上炸了才发现。一个最小可用的GitHub Actions工作流文件是这样的name: Run Tests on: push: branches: [main] pull_request: jobs: test: runs-on: ubuntu-latest strategy: matrix: python-version: [3.9, 3.10, 3.11] steps: - name: Checkout code uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: ${{ matrix.python-version }} - name: Install dependencies run: | pip install -e . pip install pytest pytest-cov pytest-mock - name: Run tests run: | pytest --covmyapp --cov-reportterm-missing这个工作流里用到了矩阵测试strategy.matrix意思是同时在Python 3.9、3.10、3.11三个版本上跑一遍测试。这样做的好处是能尽早发现版本兼容性问题。我在实际项目中曾经吃过一个亏本地用Python 3.11开发所有测试通过但线上跑的是Python 3.9结果dict合并的语法在3.9里直接语法报错。有了版本矩阵这种问题提交代码当天就会被发现。CI环节里还有几个细节值得注意。一是依赖安装要保证可重复推荐把所有测试依赖写进requirements-dev.txt不要每次CI时临时想起来装什么就写什么。二是测试命令要固定好参数比如覆盖率统计、测试路径、超时设置这些都应该沉淀到pyproject.toml里。三是CI执行时间别太长如果测试总耗时超过10分钟工程师跑完代码推送后等待反馈的过程就会变得很难受这种情况要考虑用pytest-xdist插件做并行执行pip install pytest-xdist pytest -n auto-n auto会自动检测CPU核数把测试用例分发到多个worker并行执行。我实测过一个1500条用例的项目串行要跑8分钟加上-n 4之后压缩到3分钟左右体验大幅改善。5.3 关于测试金字塔别把重心放反前文提过测试金字塔的概念这里再展开讲一下为什么重要。我在一些项目里见过这样的测试分布端到端测试一大堆单元测试寥寥无几。结果每次跑测试光启动服务、准备测试环境就要花十几分钟而且因为链路长出现失败时定位问题非常困难——可能只是接口里一个字段拼错了你却要在日志里翻半天才能锁定。合理的金字塔结构是底座是大量的单元测试速度快、覆盖精准、定位清晰中间是集成测试验证模块之间配合是否正确塔尖是少量端到端测试只覆盖最关键的核心链路比如登录注册、下单支付这类业务价值最高的路径。这个比例怎么把握粗略参考单测、集成、端到端的数量比大概在6:3:1比较合适。如果你发现自己的端到端测试数量比单元测试还多就要警惕了。端到端测试贵但也重要解决思路是把它从日常全量执行中摘出来只在发布前或者每天定时执行一次而不是每次提交代码都全量跑。6. 常见问题与排查技巧实录6.1 测试收集不到用例跑了等于白跑现象pytest执行后提示collected 0 items。这种情况九成以上是命名问题。排查顺序我一般是这样测试文件名是不是test_xxx.py或者xxx_test.pypytest默认只收集这两种命名的文件。测试函数名是不是以test_开头类名是不是以Test开头测试文件所在的目录有没有被testpaths配置排除掉测试文件是否在某个没有__init__.py的包目录里但又被当成了普通模块导入还有一个很容易被忽略的场景tests目录下有__init__.py但你不需要它。在很多老项目里为了兼容unittest的导入方式tests目录会放__init__.py。pytest在根目录扫描时如果看到目录内是普通包结构它会按包方式导入测试模块。如果包导入路径和你实际的项目结构对不上就会出现能扫描到文件但导入失败的报错而且它可能只是默默跳过不给你明显的提示。我在遇到莫名其妙的0 items问题时会直接用pytest --collect-only -v看它到底收集到了什么、在哪里失败。6.2 fixture不生效参数被当成普通参数写fixture最头疼的报错是fixture xxx not found。我排查的思路是先确认fixture函数是否定义在测试文件内、conftest.py里或者某个插件中。conftest.py是pytest的特殊文件放在某个目录下该目录及所有子目录的测试文件都可以使用它里面定义的fixture不需要手动导入。另外conftest.py的可见范围是目录层级生效的。放在tests/conftest.py的fixture对tests下所有测试文件可见但放在tests/subdir/conftest.py的fixture只对tests/subdir里的测试可见。如果你发现在子目录里找不到父目录配好的fixture多半是因为放错了层级。还有一个隐蔽的坑fixture函数名和测试函数参数名必须完全一致并且不能与pytest内置fixture重名。比如你不小心定义了一个名为tmp_path的fixture就会覆盖pytest的内置fixture导致临时目录逻辑异常。这种问题排查起来很靠经验我一般会在fixture命名上加项目前缀比如app_user、http_client降低撞名的概率。6.3 mock没生效测试还在访问真实服务这是所有mock问题里最常遇到的。根本原因前文提过patch的路径必须是被测代码实际调用时查询的命名空间。举个例子被测模块写的是import requests那你应该patchmyapp.module.requests.get被测模块写的是from requests import get那就要patchmyapp.module.get。另外一个被忽略的场景是patch装饰器顺序问题。当你在一个测试函数上堆多个mocker.patch装饰器时参数注入顺序是从下往上的——最下面的装饰器对应的参数在函数签名里排第一位。这个规则和unittest.mock.patch的标准行为一致但很多新手在这里搞反导致拿到的mock对象张冠李戴。# 注意参数顺序装饰器从下往上对应参数 mock.patch(myapp.service.email_sender) mock.patch(myapp.service.logger) def test_service(mock_logger, mock_email_sender): # 这里 mock_logger 对应上面的 logger而不是 email_sender pass6.4 测试结果不稳定时好时坏如果测试用例上次跑通过、这次跑失败而且没有改动过业务代码那大概率是测试环境里有共享可变状态。最常见的元凶有几类测试依赖了真实的系统时间跨天、跨小时后行为变化。测试用到了全局随机数不同次执行产生不同数据。测试操作了真实文件或真实数据库上一条用例改掉的数据影响了下一条用例。测试之间共享了同一个fixture对象而fixture是可变的。对应的解决策略分别是时间依赖用freezegun库固定时间或者在函数设计时把时间作为参数传入随机数在用random之前手动设置random.seed(42)制造可复现性文件和数据库操作统一使用tmp_path和事务回滚fixture默认用scopefunction确保每个测试拿到的都是全新对象。6.5 测试代码本身越来越难维护测试写到一定规模后维护成本会急剧上升很多人因此放弃写测试非常可惜。我复盘自己维护长线项目的心得总结了三个让测试可持续的原则一是测试数据尽量集中管理。把构造用户、构造订单、构造配置文件这类通用数据准备逻辑提炼成工厂函数或fixture不要让每条测试里散落一堆魔法数字。二是一条测试只验证一个行为。如果一个测试函数里面既查业务结果、又查权限校验、又查日志输出一旦失败很难定位而且后期需求变更时只要其中一个行为变了测试就得大改。三是测试代码也要做代码审查。测试是固定资产它的可读性和业务代码同等重要不对测试代码做Review时间久了它就会腐烂成没有人敢动的雷区。从踩坑到习惯我的一点个人体会做了这么多年开发回头看测试这件事我最大的感受是写测试不是额外的工作量而是一种思维方式的转变。写业务代码时你站在实现者的角度关心怎么把功能做出来写测试时你被迫站在审查者的角度去想输入有哪些边界、异常会发生在哪里、其他模块怎么依赖这段逻辑。这种换位思考本身就会逼着你写出更健壮的代码。如果你现在刚开始在项目里引入测试我的建议是不要一上来就追求全面铺开。挑一个你最担心出错的模块用pytest把核心逻辑保护起来感受一下改代码不被吓到的安全感。等习惯了这个节奏再逐步扩大覆盖范围。测试不会让你多写多少代码但它会让你每一次提交都有底气。这套工程习惯越早养成后面受益越大。
返回列表