ARTICLE DETAIL

资讯详情

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

pytest测试架构从零搭建:fixture、参数化与接口自动化实战

pytest测试架构从零搭建:fixture、参数化与接口自动化实战 聊到 pytest很多人的第一反应是“Python 里挺好用的一个测试框架”。但如果你真的拿它去搭一套支撑几十个接口、上千条用例的自动化测试架构就会发现“能用”和“好用”之间还隔着一整座山——用例怎么组织、数据怎么管理、前置条件怎么复用、失败的用例怎么一眼看出问题。这篇文章我想从自己落地 pytest 测试架构的经验出发讲清楚这套东西怎么从零开始搭为什么这么搭以及我踩过的哪些坑你不需要再踩一遍。适合正在做 Python 接口自动化或单元测试的同学参考尤其是测试开发、测试工程师以及负责项目质量保障的后端开发。内容偏实战我会把完整的目录结构、 fixture 设计思路、请求层封装、参数化方案、报告接入和真实遇到的问题全部整理出来你可以直接照着抄也可以在这个基础上根据自己的项目调整。1. 从“能跑”到“能维护”pytest 架构到底在解决什么问题1.1 为什么需要一套测试架构而不是一堆测试用例我刚接触 pytest 的时候写测试用例的方式特别原始一个 test_ 开头的文件里从上到下堆十几个函数每个函数里复制粘贴同一套登录逻辑、同一个请求地址、同一段重复的断言。刚开始用例少跑起来挺快等用例到了两三百条的时候问题就开始集中爆发。第一是维护成本爆炸。接口地址变了你得全局搜索替换登录 token 失效了每一条用例都跟着挂请求参数结构变了要挨个改。第二是排查成本高。一条用例失败了控制台只告诉你“断言失败”但你根本不知道是数据问题、环境问题还是代码逻辑问题。第三是没法扩展。你想跑一个冒烟集、想并行执行、想接入 CI持续集成发现测试代码和业务逻辑搅在一起根本没法改。所以测试架构的核心不是“写用例”而是“管理用例”。一个好的 pytest 架构必须做到几条用例之间独立、不互相依赖公共逻辑抽离、能复用数据、配置、代码分离跑完以后的结果足够清晰能快速定位问题。这个思路其实和写业务代码是一样的——单一职责、开闭原则、高内聚低耦合只不过业务代码处理的是用户请求测试代码处理的是测试行为和断言结果。1.2 一个可以落地的四层目录结构这里我给出一份我觉得比较成熟、也适合多数 Python 项目的目录结构它把数据、接口封装、业务断言和用例分成了四个层次project_root/ ├── pytest.ini # pytest 全局配置 ├── requirements.txt # 项目依赖 ├── conf/ │ ├── __init__.py │ ├── path_config.py # 路径配置 │ ├── read_config.py # 配置文件读取 │ └── env_config.yaml # 环境配置dev/test/prod ├── common/ │ ├── __init__.py │ ├── base_class.py # 公共方法封装纯函数式 │ ├── md5_encrypt.py # 加密处理 │ ├── observer.py # 观察者模式可做屏幕截图与数据钉钉通知 │ ├── py_mysql.py # MySQL 操作 │ ├── data_mysql.py # 数据准备与依赖调用 │ ├── dingtalk.py # 钉钉推送 │ └── ... ├── data/ │ ├── test_case_data.xlsx # 用例数据 │ └── ... ├── report/ │ ├── html/ # 测试报告 │ ├── log/ # 日志 │ ├── screenshot/ # 失败截图 │ └── allure/ # allure 原始数据 ├── test_case/ │ ├── __init__.py │ ├── conftest.py # 根级 fixture全局共享登录态等 │ ├── test_user_module/ # 模块化用例目录如用户模块 │ │ ├── __init__.py │ │ ├── conftest.py # 模块级 fixture │ │ └── test_login.py │ └── test_order_module/ # 订单模块 │ └── ... └── main.py # 统一入口可指定运行目录这份结构我实际跑过很多个项目改动过很多版最终留下的核心原则就一句话用例目录只放测试用例不想干的东西全部抽出去。common/放与业务无关的工具函数data/只放测试数据文件测试代码里不出现裸的open()之类操作统一走封装层。这样项目规模变大以后新人接手也容易上手。2. conftest 与 fixture这套架构的地基2.1 fixture 的作用域、autouse 与依赖管理如果只用一句话回答“pytest 里最有价值的功能是什么”我的答案一定是 fixture。它是 pytest 依赖注入机制的实现用来解决测试前置条件、测试环境准备、数据清理、资源回收这类问题。fixture 有几个基本知识点作用域function每个函数、class每个类、module每个模块、session整个测试会话。默认是 function。autouseTrue即使用例函数不显式声明这个 fixture也会自动执行适合做全局初始化、环境检查等。fixture 可以调用另一个 fixture形成依赖链pytest 会自行解析依赖顺序。示例如下import pytest pytest.fixture(scopesession) def base_url(): return https://api.example.com pytest.fixture(scopesession) def session_token(base_url): # 依赖 base_url登录拿到 token token login_and_get_token(base_url) return token pytest.fixture(autouseTrue) def env_check(base_url): print(f当前环境: {base_url}) # 自动执行无需用例显式声明这里要提醒一点fixture 的依赖关系尽量不要嵌套过深。我见过一个项目里一个 fixture 依赖了六个其他 fixture最后完全没法排查问题。控制依赖深度、每个 fixture 只做一件事比什么都重要。2.2 用 fixture 做前置条件与数据准备接口自动化测试里前置条件是个高频场景。比如“登录成功”和“用户已下单”一个下单接口的用例可能需要先调用户模块创建一个用户再调订单模块创建一笔订单然后才能测目标接口。如果把这些前置逻辑写在用例函数里一方面大量重复另一方面每条用例一复制一粘贴后面维护就是灾难。用 fixture 可以这样拆import pytest import requests pytest.fixture def auth_token(): url https://api.example.com/login resp requests.post(url, json{user: tester, passwd: 123456}) assert resp.status_code 200 return resp.json()[token] pytest.fixture def created_order(auth_token): 创建一笔测试订单返回订单 id url https://api.example.com/orders headers {Authorization: fBearer {auth_token}} resp requests.post(url, json{product_id: 1001, count: 2}, headersheaders) assert resp.status_code 200 order_id resp.json()[order_id] # 用例结束之后自动清理 yield order_id requests.delete(f{url}/{order_id}, headersheaders) def test_submit_payment(auth_token, created_order): # 现在用例函数里只需要关注支付流程本身 resp requests.post( https://api.example.com/payments, json{order_id: created_order}, headers{Authorization: fBearer {auth_token}}, ) assert resp.status_code 200注意我在created_order里用了yield而不是return这是 fixture 实现 teardown 的标准姿势。yield后面的代码会在用例结束时执行适合做数据清理。经验fixture 的清理逻辑一定不要省略。很多测试团队跑挂一套环境就是因为测试数据越积越多像是在一个池子里不断丢垃圾。就算没有现成的删除接口至少把落库的脏数据用 SQL 清掉。2.3 用 fixture 做登录态共享接口自动化里让人最头疼的就是登录 token。绝大多数接口都需要鉴权每条用例都走一次登录流程不现实也不合理。实践上我会把登录 token 做成一个 session 级别的 fixture整个测试会话只登录一次然后通过请求会话把 token 自动带到后续所有请求里。# test_case/conftest.py import pytest import requests pytest.fixture(scopesession) def auth_headers(): 整个测试会话只登录一次返回带鉴权的请求头 login_url https://api.example.com/login resp requests.post(login_url, json{user: ci_user, password: ci_pwd}) resp.raise_for_status() token resp.json()[token] return {Authorization: fBearer {token}} pytest.fixture def api_client(auth_headers): 封装一个带鉴权的 requests 会话每条用例可直接使用 session requests.Session() session.headers.update(auth_headers) return session这里有两个细节值得注意第一登录失败时要raise_for_status()确保初始化阶段就能暴露环境问题而不是等用例执行到一半才报 401那样排查起来非常痛苦。第二token 的过期时间要心里有数。如果测试数据量大、执行时间长一个 session 级别的 fixture 可能跑到一半就过期了。两种方案一是把 scope 从 session 降为 module二是让请求层在收到 401 时自动重新登录重试一次。第二种更优雅我会在请求层封装里讲。2.4 PyCharm 里跑 pytest 的常见问题和配置很多初学者装了 pytest 以后发现 PyCharm 的右键运行按钮是灰色或者直接跑的 unittest就很懵。这里有一个最简单的配置方式。在 PyCharm 中打开 Settings → Tools → Python Integrated Tools → Testing把 Default test runner 改成 pytest。这样右键点击测试文件或方法默认就会以 pytest 方式执行。但要注意一个细节如果项目里同时存在 unittest 风格用例测试类继承 TestCase、方法名 test_ 开头PyCharm 默认执行器可能会把两套用例一起发现和执行产生一堆奇怪的行为。所以项目里尽量统一一种测试风格。我自己的项目就明确要求新写的用例全部用 pytest 原生的函数式写法不用 unittest 风格类。另外PyCharm 运行 pytest 时会自动往命令里注入一些参数跟 pytest.ini 或 pyproject.toml 里的配置叠加。如果你发现本地跑不过、但命令行能跑过多半是 PyCharm 注入的默认选项导致的比如--no-header、--disable-warnings等。这种情况可以先在 Run/Debug Configurations 里清空 Additional Arguments再看是不是配置冲突。3. 接口自动化的关键请求层封装与参数管理3.1 请求会话与会话夹具的设计接口自动化的框架跟 UI 自动化不一样的地方在于UI 自动化可能是靠定位元素去驱动而接口自动化本质上就是各种 HTTP 请求的组合。所以请求层是整座架构里最核心的地基请求层设计得好不好直接决定用例代码的洁癖程度。我推荐的思路是不直接让用例函数去调requests.get()或requests.post()而是封装一个 API 客户端把 URL 拼接、header 注入、日志记录、响应统一校验都收进去。这样用例代码只需要关注业务参数和断言即可。# common/api_client.py import requests import logging logger logging.getLogger(__name__) class ApiClient: def __init__(self, base_url: str, headers: dict None): self.session requests.Session() self.base_url base_url.rstrip(/) if headers: self.session.headers.update(headers) def request(self, method: str, path: str, **kwargs): url f{self.base_url}/{path.lstrip(/)} # 记录请求日志 logger.info(fREQ {method.upper()} {url}) if json in kwargs: logger.info(fBODY {kwargs[json]}) resp self.session.request(method, url, **kwargs) logger.info(fRESP {resp.status_code} {resp.text[:500]}) return resp def get(self, path: str, **kwargs): return self.request(GET, path, **kwargs) def post(self, path: str, **kwargs): return self.request(POST, path, **kwargs)这段代码里日志是很关键的一环。我在项目里吃过很多亏一条用例失败了看控制台只有一行断言错误完全没有上下文根本不知道请求发给了谁、响应返回了什么。封装请求层以后每条请求的 URL、请求体、响应状态和响应体第一段都会自然落到日志里排查问题效率高了一个量级。3.2 多环境配置与动态切换多环境是接口自动化不可回避的问题。测试环境、预发布环境、生产环境的域名和账号权限都不一样。很多项目喜欢在代码里硬编码http://127.0.0.1:8000这种地址换环境要全局改这是最需要避免的。我建议准备一个环境配置文件pytest 启动时通过环境变量或命令行参数来决定使用哪一套配置。# conf/env_config.yaml dev: base_url: http://dev.api.example.com user: dev_tester password: dev_pwd test: base_url: http://test.api.example.com user: test_tester password: test_pwd然后在 conftest 里做一个环境选择的 fixtureimport os import yaml import pytest def load_env(env_name: str) - dict: with open(conf/env_config.yaml, r, encodingutf-8) as f: all_env yaml.safe_load(f) return all_env[env_name] pytest.fixture(scopesession) def env_config(): env_name os.getenv(TEST_ENV, test) return load_env(env_name) pytest.fixture(scopesession) def api_client(env_config): client ApiClient( base_urlenv_config[base_url], headers{Authorization: load_token(env_config)}, ) return client这样在命令行运行时只需要TEST_ENVtest pytest -v本地调试默认走 test 环境CI 部署时通过环境变量指定对应环境代码里不需要改任何配置。注意配置文件如果包含真实密码和密钥一定要加入.gitignore不要在 Git 仓库里裸存放。可以用类似.env.example的方式提供配置模板让执行者自己复制填写。3.3 在 PyCharm 中运行时的环境变量问题有个很常见的坑在命令行设置了TEST_ENV但在 PyCharm 里右键运行时环境变量并不存在于是代码用了默认的 test 环境。这不一定是错但如果你改了.env或系统环境变量后PyCharm 里怎么都不生效大概率是 PyCharm 缓存了旧环境变量。此时有两个办法。一是重启 PyCharm二是直接在 Run/Debug Configuration 的 Environment variables 一栏里手动填入TEST_ENVtest。后者更可控也方便不同配置间切换。4. 参数化、断言与用例组织规范4.1 参数化的三种常用姿势pytest 的parametrize是这套框架里最让我省时间的功能。同样的接口输入十组不同参数不需要写十个函数一个函数 参数化直接搞定。最基础的是直接参数化import pytest pytest.mark.parametrize(username,password,expected, [ (tester, 123456, 200), (tester, wrong_pwd, 401), (, 123456, 400), ]) def test_login(username, password, expected): resp api_client.post(/login, json{user: username, passwd: password}) assert resp.status_code expected第二种是带ids的用法给每组数据一个可读的名字。不加 ids 时pytest 生成的用例名是一堆参数值的拼接根本看不懂哪条是哪条。加了 ids 之后报告的可用性瞬间提升。pytest.mark.parametrize( username,password,expected, [ (tester, 123456, 200), (tester, wrong_pwd, 401), (, 123456, 400), ], ids[正确密码, 错误密码, 空用户名], ) def test_login(username, password, expected): ...第三种是参数化的数据从外部文件读取。数据量大的时候不要堆在代码里我建议放到 Excel 或 JSON 文件然后用一个公共函数读出来再传进parametrize。# data/login_data.json [ {username: tester, password: 123456, expected: 200}, {username: tester, password: wrong, expected: 401} ]import json import pytest def load_login_data(): with open(data/login_data.json, r, encodingutf-8) as f: return json.load(f) pytest.mark.parametrize(case, load_login_data()) def test_login(case): resp api_client.post(/login, json{ user: case[username], passwd: case[password], }) assert resp.status_code case[expected]这里有一个细节load_login_data()是在收集阶段就会执行。如果文件路径写错或者 JSON 格式非法pytest 会在收集阶段直接报错而不是等到测试执行的时候。对使用者来说这其实是好消息——问题暴露得更早。4.2 断言规范让失败的用例一秒看懂断言是测试的灵魂但很多人写断言非常敷衍动不动就assert True或者整个用例只断言一个状态码 200其他什么都不管。这样的用例跑再多遍对质量的保护作用也有限。我自己的断言规范有几点每个关键返回字段至少断言一次状态码不是唯一标准。断言信息要带上下文比如assert resp.status_code 200, f登录失败, 实际状态码: {resp.status_code}, 响应: {resp.text}。对返回数据里的核心业务字段单独判断而不是笼统地判断整段 JSON 相等JSON 里字段顺序变化就能让整体相等断言失败误报率高。def test_get_order_detail(): resp api_client.get(/orders/1001) assert resp.status_code 200, f订单详情接口异常: {resp.status_code}, {resp.text} data resp.json()[data] assert data[order_id] 1001 assert data[status] in (PENDING, PAID, SHIPPED), f订单状态异常: {data[status]} assert data[total_amount] 0如果项目有频繁用到的时间断言、金额断言可以封装几个公共断言方法放到common/里比如assert_greater_than(value, threshold)、assert_in_time_range(value, start, end)。这样既统一了断言逻辑也避免每个用例各写各的。4.3 pytest.ini 的常用配置项pytest.ini 是我每次建新项目都要最先创建的配置文件它决定了 pytest 的运行行为和规则。这里放一份我觉得覆盖度比较够的配置[pytest] minversion 6.0 testpaths test_case python_files test_*.py *_test.py python_classes Test* python_functions test_* addopts -v -s --tbshort -q filterwarnings ignore::DeprecationWarning markers smoke: 冒烟测试用例 slow: 慢速用例默认不执行testpaths指定测试用例目录避免 pytest 在项目根目录里到处找尤其要避免它把venv、site-packages里的文件也扫描进去。python_files、python_classes、python_functions控制发现规则。通常保持默认就能满足需求除非你的项目里有老代码用了非标准命名。addopts默认执行参数。-v便于看每条用例结果-s允许 print 输出--tbshort让 traceback 更简洁。filterwarnings屏蔽掉那些无意义的警告让报告干净一点。不要一开始就屏蔽所有警告建议等确定哪些是第三方库的噪音之后再配。markers这是很容易被忽略的配置。如果你的用例里用了pytest.mark.smoke但没有在 markers 里声明pytest 会给你一个 PytestUnknownMarkWarning 警告。声明之后不仅警告消掉你还可以用-m smoke只跑冒烟用例非常方便。4.4 pytest 与 pytest-html 报告生成测试报告是测试工程师交给团队的“产品”报告质量直接影响别人对你工作的信任度。我早期用 pytest 自带的输出截图给领导看被吐槽过“看不懂没法判断通过率”。后来接入了 pytest-html情况好了很多。安装pip install pytest-html运行时指定生成 htmlpytest --htmlreport/html/report.html --self-contained-html--self-contained-html很关键它会把 CSS、JS 都打包进一个 html 文件里直接双击就能在浏览器打开不需要额外启动服务。不过又要说一个坑pytest-html 默认的 report 不带请求和响应信息。如果你想让每一条失败的用例自动带上接口响应需要自己写插件扩展测试报告或者用 allure 的 attachment 把请求响应写进去。5. 进阶技巧参数化组合、插件与 hook5.1 自定义 pytest 插件一条 hook 解决失败日志采集pytest 的 hook 机制是它扩展性的灵魂。很多人在网上找各种插件但其实一个几十行的自定义 hook 能解决很多定制需求。举个例子有一次团队希望“用例失败时自动把完整的接口请求和响应追加到报告里并截取一张当前页面的截图”UI 自动化还好说接口自动化的截图不太适用但需求点很明确失败时必须要有足够的现场数据。我用pytest_runtest_makereport写了一个很小的插件# common/pytest_enhance.py import pytest pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield rep outcome.get_result() if rep.when call and rep.failed: # 从 item 的 fixture 实例里取请求信息和响应信息 client item.funcargs.get(api_client) if client: rep.longrepr ( f{rep.longrepr}\n\n f最近一次请求: {client.last_request}\n f最近一次响应: {client.last_response} )这段代码的逻辑是在 pytest 的“生成测试报告”阶段插入自己的逻辑如果用例失败就把请求客户端里记录的最后一次请求和响应内容追加到失败信息里。实际落地后失败用例的排查效率提升非常大基本不需要再重跑一遍用例。5.2 fixture 中的工厂模式与间接参数化有一种更复杂的场景接口 A 的返回结果要作为接口 B 的输入但参数化数据每次不同fixture 不能写死。这时候可以用工厂模式——让 fixture 返回一个函数而不是返回一个固定值。import pytest pytest.fixture def create_order(api_client): created_ids [] def _create_order(product_id, count1): resp api_client.post(/orders, json{ product_id: product_id, count: count, }) resp.raise_for_status() order_id resp.json()[order_id] created_ids.append(order_id) return order_id yield _create_order # 用例结束后清理全部创建的订单 for order_id in created_ids: api_client.delete(f/orders/{order_id}) def test_pay_order(create_order): order_id create_order(1001, 2) # 接下来对 order_id 做业务操作这种“工厂返回函数 yield 批量清理”的模式在实际项目中非常实用。它允许每个用例按自己的需求灵活创建数据又不会到处留垃圾数据。还有一种是indirectTrue的参数化。当参数不是直接传给测试函数而是传给 fixture 时用pytest.fixture def user(request): return request.param pytest.mark.parametrize( user, [{name: alice, age: 18}, {name: bob, age: 20}], indirectTrue, ) def test_user_age(user): assert user[age] 18这个特性适合“同一个参数要驱动多个 fixture”的场景比如测试环境切换和账号切换组合起来的时候。5.3 多进程执行与失败重试用例多了以后串行执行会变得很痛苦。我有一次跑了三百多条接口用例串行用了接近四十分钟后来接入 xdist 多进程时间直接砍到七分钟。安装和使用pip install pytest-xdist pytest -n 4 # 4 个进程并行这里有个隐藏的坑多进程执行时每个进程都会创建一个进程内状态。如果 fixture 里有写文件的逻辑多个进程同时写同一个文件很可能相互覆盖。解决办法是要么把写文件的部分抽到 conftest 的 session 级别 fixture 里只在主进程执行要么每个进程写不同后缀的文件。失败重试我用的是 pytest-rerunfailurespip install pytest-rerunfailures pytest --reruns 2 --reruns-delay 1它解决的问题是偶发性的网络抖动、服务短暂波动导致的偶发失败。但是我要强调一点重试次数不要设太多--reruns 2就已经够了。如果一条用例重试 5 次才通过说明它本身就有问题大概率是测试数据污染或者断言写法不对应该去修用例而不是让重试机制把问题掩盖掉。5.4 pytest 的插件生态概览用 pytest 这几年我沉淀下来一套固定搭配插件用途值得一提的点pytest-xdist多进程并行-n auto自动用满 CPU 核心pytest-rerunfailures失败重试配合--flaky标记跳过敏捷集pytest-htmlHTML 报告--self-contained-html生成单文件报告pytest-ordering用例执行顺序控制pytest.mark.run(order1)pytest-timeout单条用例超时控制防止接口 hung 住导致整套卡死allure-pytestAllure 报告生态完善适合团队级平台注意pytest-ordering这个插件我其实现在用得越来越少。用例最好是不依赖顺序的一旦依赖顺序后面维护就会很痛苦。如果确实需要优先级我建议用-m smoke先跑冒烟再跑全量而不是强制指定用例执行顺序。6. 常见问题排查与避坑实录6.1 用例收集不到或执行 0 条我有一次在 CI 上发现 pytest 跑出了 “no tests ran”本地却正常。排查后发现是 CI 的python_files配置没生效pytest 只认特定前缀的文件名。如果你的用例文件叫做api_test_login.py那不在默认的test_*.py和*_test.py匹配范围内。解决方法把用例文件统一命名为test_开头比如test_login.py。或者在 pytest.ini 的python_files里加上api_test_*.py。这种问题属于一击致命型——不会报错但根本不出结果。所以每次配置变更后先跑一条最小用例确认收集正常再全量执行。6.2 用例执行顺序与依赖导致的偶发失败接口自动化里最常见的偶发失败就是“用例之间互相依赖”。比如用例 A 创建了订单用例 B 默认订单已存在当只单独跑 B 时就失败了。解决思路是用例必须自带数据准备和清理不依赖任何其他用例的执行产物。如果实在避不开依赖那就在 fixture 里做“懒处理”用例执行前检查数据是否存在不存在则先造一条用完再清理。6.3 中文输出乱码问题pytest 在 Windows 终端下的中文输出尤其是断言失败信息里包含中文字段很容易乱码。这个问题跟 pytest 本身关系不大更多是终端编码的问题。命令行可以设置set PYTHONIOENCODINGutf-8PyCharm 则在 Settings → Editor → File Encodings 中把 IDE Encoding 和 Project Encoding 都改成 UTF-8。另外所有读取文件的地方open()都要写encodingutf-8这是最容易被忽略的——一是配置文件读取二是测试数据读取。6.4 pytest 运行完内存不释放或者跑得越来越慢如果你的用例里创建了很多 fixture 资源尤其是 session 级别的客户端或数据库连接没有正确关闭pytest 会随着用例数量增加而越跑越慢。我的排查思路是检查 session 级别和 module 级别的 fixture确认它们的 teardown 逻辑里有没有正确的close()或pool.dispose()。用pytest --duration5输出最慢的 5 个测试看是哪些用例在拖慢整体时间。大量失败重试会成倍增加执行时间确认重试次数是否合理。6.5 pytest 常用问题速查表现象可能原因快速解法用例显示 0 collected文件名不匹配或 testpaths 路径不对检查python_files配置和testpaths运行成功但没有执行用例-k表达式匹配不上先去掉-k试试fixture 报依赖错误fixture 名称拼写错误或作用域冲突检查 fixture 的函数名和参数名断言失败却没有请求日志请求层没有注入日志在请求封装中用logging记录请求响应报告没有样式直接打开了非--self-contained-html的 html加--self-contained-html重新生成并行执行时数据混乱多个进程操作了同一份测试数据给每个进程隔离数据或串行执行有共享数据的用例本地通过 CI 失败环境变量、配置文件或依赖版本不一致对比 CI 的 Python 版本、pip 依赖和 env 配置6.6 运行时的三个实用技巧最后分享三个我每天都在用的小技巧。技巧一只跑部分用例。不用每次改动都全量回归可以按路径、按标记、按关键字精确筛选pytest test_case/test_user_module/test_login.py -v # 跑单个文件 pytest test_case/test_user_module -k login -v # 文件名或用例名中包含 login pytest -m smoke -v # 只跑冒烟标记技巧二定位慢用例。用例多了以后整体执行时间飙升用下面的命令看排名前几的慢用例pytest --durations5技巧三调试单条用例。想用 pdb 进入断点pytest test_case/test_login.py::test_login_error --pdb7. 踩过坑之后的几点体会文章写到这内容已经比较完整了但这些全部是我在实际项目里一点点试出来的有些教训现在想起来还觉得肉疼这里再补几句个人体会。第一点是关于 fixture 的中文文档和网上各种教程看再多不如自己动手搭一遍。很多人喜欢收藏各种文章但收藏不等于掌握。我建议你找一个没有任何测试代码的旧项目花半天时间把上面这套目录结构和 fixture 体系照着重写一遍跑通一个最小闭环再往里加自己的业务逻辑。这个过程比看十篇教程都有效。第二点是测试用例的“最低通过标准”。我见过很多测试同学为了让用例“看起来全绿”大量使用 try-except异常吞掉后直接返回 True。这是很危险的事情。测试用例的价值就在于它能暴露问题你把它捂住了系统就真的可能带病上线。用例失败不是坏事应该在失败后定位、修复、回归而不是想方设法让它变绿。第三点是关于测试代码的维护投入。很多人只把测试当“副产品”写出来就不管了。但测试代码和业务代码一样需要重构和迭代。接口改了用例不改两天后这套测试就跑不动了测试数据和环境配置变更了文档不更新新同学接手就得重新踩一遍坑。后面你会发现维护一套让全团队都愿意用的测试架构比写一千条测试用例更值钱。
返回列表