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

企业级conftest.py重构实战:从膨胀到分层治理

在接手过几个中型以上的测试团队项目之后我越来越意识到一个规律几乎所有测试工程腐烂的起点都是同一个文件——conftest.py。用pytest写过几年测试的人应该都有这种体验项目刚起步时conftest.py只有几十行放两个fixture就能跑通全部用例。半年后它开始悄悄膨胀八个月后你发现它已经快两千行里面什么都有session级别的数据库连接、各类webdriver初始化、封装的请求客户端、各种hook函数、记录日志的装饰器、甚至还有从业务代码里拷来的工具函数。没人敢轻易动它因为你根本不知道这个文件里某个看似多余的fixture到底被多少个测试用例依赖着。这个文件一旦失控整个测试工程的可维护性就开始断崖式下跌。这篇文章就以重构企业级conftest.py为切入点把我在实际项目中梳理出来的一套完整重构思路、具体操作步骤和踩坑经验整理出来希望能给同样被这个文件折磨的人一些参照。1. 重构前先摸清家底你的conftest.py病到什么程度了1.1 企业级conftest.py的典型症状我先说说什么样的conftest.py算“企业级”。字面意义有点唬人其实就是测试用例数量上千、参与开发的人超过三五个、测试环境不止一套、对接的第三方服务好几个。这个规模下conftest.py如果没有任何治理痕迹几乎必然出现下面这些症状。第一个症状是单一文件膨胀。我从一个真实项目里看到过一份conftest.py2600多行包含70多个fixture、十几个hook函数、5个辅助类。这个文件本身就很难浏览编辑器打开都会卡。更麻烦的是它里面的fixture名称大量重复比如同一个模块的数据库连接在session级别、module级别、function级别各定义了一份。出现这种情况的原因是不同开发者在不同阶段各写各的谁也不知道别人已经定义过类似的东西干脆再加一个最后自然是灾难。第二个症状是fixture的作用域滥用。我见过一种很典型的使用方式为了省事把几乎所有的fixture都定义成pytest.fixture(scopesession)。session级别的fixture只在测试会话开始时执行一次看起来效率很高但代价是状态在用例之间被共享。一旦某个用例把数据库里的数据改了后面几百个用例跑的就是“被污染”的状态。排查这种问题非常痛苦因为你不确定是哪条用例先改了数据只能靠二分法不断缩小范围运维成本极高。第三个症状是hook函数和工具函数的混入。有些项目会直接把pytest_collection_modifyitems排序逻辑、失败重试逻辑、Allure报告整理逻辑全部塞进conftest.py再把生成随机字符串、解析配置文件、读取环境变量这些小工具也一起写下边。发这个文件的人出于好心觉得“反正都是给测试用的放在这里大家都能用”。可一旦连“去找某个工具函数都要搜索半天”的时候这个文件就已经变成了一座没人理得清的地下仓库每个人都在往里面扔东西却没有人在做整理。第四个症状是隐式依赖严重。autouseTrue的fixture数量超过五六个以后新来的同事基本只能靠翻代码来猜哪些用例被偷偷注入了什么。一旦想删掉某个autouse fixture你没法通过搜索“这个fixture名在哪些用例里出现”来判断影响面因为用例代码里可能压根没有引用它。这是重构时最棘手的一种技术债因为它的连锁反应会扩散到整个测试套件而不只是某一个文件。1.2 体检的四个维度与判断标准在动手重构之前我建议先给现有conftest.py做一次“体检”用数据来支撑后续的拆分决策而不是凭感觉。第一个维度是行数和“密度”。打开文件统计总行数再看fixture平均行数。如果一个文件的80%内容都在定义fixture其余20%塞了hook、常量、工具函数基本可以确定需要拆分。我一般用两条硬性标准超过800行的conftest.py就该启动重构评估超过1500行必须重构。这条标准听起来很武断但实际执行下来几乎没有例外。第二个维度是fixture依赖关系。逐个fixture记录它依赖了哪些其他fixture、被哪些fixture依赖、在哪些用例中被引用。可以用pytest自带的命令快速拉一个清单我在实操部分会详细说这里先提思路pytest的fixture机制会形成一个有向无环图你把这个图画出来凡是出现在图中间位置、同时被非常多层fixture依赖的节点就是最需要优先治理的对象。比如一个session级的db_engine被几十个fixture间接依赖它就是所有依赖的源头必须先把它抽出来。第三个维度是作用域分布。把现有fixture按function/module/class/session统计个数。正常情况下一个设计良好的测试工程里function级别fixture应该占绝大多数session级别应该只用来准备那些真正全局共享、且只读的资源——比如配置文件、全局唯一的外部服务连接。如果你的session级别fixture超过总数的两成说明很多本应该独立化的状态被过早共享了。作用域选大本质上是拿隔离性换性能这在企业级项目里往往是得不偿失的。第四个维度是复用情况。把名字相同的fixture、功能相似的fixture列出来统计有多少测试模块自己又额外定义了完全重复的fixture。这种重复通常是在“不知道conftest.py里已经有现成的”这个前提下发生的。检查手段是靠代码检索工具搜索所有测试文件里pytest.fixture的定义人工比对一轮。很多团队的重复率高达三成这意味着重构后光是删除重复代码就能让测试收集时间快一大截。体检做完你会得到一张Excel表或者一份文档里面列着现有fixture清单、每个fixture的实现行数、依赖方向、作用域、被引用范围。有了这份清单接下来划分模块才有依据。没有这份清单就直接重构本质上是在赌运气。2. 重构方案选型不是把大文件拆成小文件这么简单2.1 为什么不能简单按行数切分很多人一听到重构第一反应就是把conftest.py从中间咔嚓一刀切成两个文件。这种做法的确能让每个文件的行数降下来但如果没有解决依赖和边界问题半年后每个子文件又会膨胀成新的“小conftest”。拆分不是目的让每个模块的职责清晰、边界稳定、可独立维护才是目的。我在早期的项目里也犯过这个错误。当时把conftest.py按“前半部分fixture、后半部分hook”一刀切结果两个文件之间互相引用然后必须靠pytest_plugins来来回回加载最后因为加载顺序问题到处报错折腾了一周。后来想明白了切分文件的核心是按照“领域”而不是“行数”来切。fixture和它管理的外部资源要放在一起不是把所有fixture放在一起。一个webdriver初始化的fixture就该和浏览器相关的一整套机制放在同一个模块而不是和数据工厂fixture排排坐。2.2 分层架构怎么设计结合多个项目的重构经验我建议采用“三层结构 领域模块”的组合方式。这套结构不是凭空想的而是从后端框架的分层思想借鉴过来的测试工程同样需要分层的概念。第一层是全局层对应测试目录最顶层的conftest.py。这层只放三类东西pytest插件的注册、全局级别的hook函数、极少数真正全局共享且只读的session fixture比如统一的临时目录策略、全局性的命令行参数注入。你只要让这一层精简到不做任何具体业务它就很难再腐化。每次有人想把新东西塞进顶层conftest.py时都应该先问一句这个东西是所有模块都要用的吗如果不是请下移到领域层。第二层是领域层对应每个业务模块目录下的conftest.py。这一层可以放该模块专用的fixture和hook作用域以module为主负责给这个模块的测试用例提供贴近业务场景的组件。比如订单模块的目录下存放订单状态构造的fixture用户模块下存放登录态相关fixture。这样做的收益是不同业务域的测试代码互不干扰订单模块改了自己的fixture不会惊动用户模块的测试。第三层是共享层对应专门的fixtures包里面再按具体领域细分比如fixtures.db、fixtures.auth、fixtures.web、fixtures.data。这一层的模块通过pytest_plugins机制被全局层加载供所有测试目录使用。凡是多个模块都会用到的东西放在这一层而不是放在某一个业务模块的conftest里。这套结构的核心思想是越通用的内容层级越浅越业务化的内容越靠向具体模块。测试代码和业务代码一样也需要依赖注入和分层边界只是很多人写测试时没有这个意识久而久之就形成了“一个大文件管所有事”的局面。2.3 具体目录结构长什么样我给出一个在实际项目中验证过的例子大家可以根据自己的业务替换模块名。tests/ ├── conftest.py ├── fixtures/ │ ├── __init__.py │ ├── config.py │ ├── db.py │ ├── auth.py │ ├── web.py │ └── data.py ├── hooks/ │ ├── __init__.py │ ├── report.py │ └── retry.py ├── test_order/ │ ├── conftest.py │ ├── test_create.py │ └── test_query.py └── test_user/ ├── conftest.py ├── test_login.py └── test_profile.py注意这个结构里我把hooks单独放了一级。有读者可能会问hook函数为什么不能放在fixtures包顶层因为hook函数是在pytest框架生命周期里运行的钩子作用在“测试收集、执行、报告”这些环节和fixture这种“为用例提供依赖”的能力是两回事。放到一起还是职责混淆。hooks目录里每个文件聚焦一种运行行为report负责报告整理retry负责失败重试需要在不同阶段接管的pytest事件各管各的互不越界。顶层的conftest.py在我的方案里只负责两件事注册pytest_plugins、继承那些必须全局生效的hook。它不再直接定义任何业务fixture。我实际测下来的效果是顶层conftest.py通常不超过60行非常清爽。# tests/conftest.py import pytest pytest_plugins [ fixtures.config, fixtures.db, fixtures.auth, fixtures.web, fixtures.data, hooks.report, hooks.retry, ] pytest.hookimpl(tryfirstTrue) def pytest_configure(config): 全局配置入口只在配置阶段执行一次。 ...顺带提醒一句pytest_plugins里的路径是模块路径不是文件路径且这套配置强烈建议放在测试根目录顶层的conftest.py里。放在子目录的conftest.py中时pytest的模块加载机制容易引发ImportError这是官方文档里明确标注过的坑我在下文排查部分再展开。3. 实操企业级conftest.py的完整重构演练3.1 盘点与分类先给fixture做台账现在开始动手。第一步是在重构前把现有内容全部盘点清楚我会用一个具体例子带着大家走一遍流程。假设你的项目现在有一个tests/conftest.py1200行。先用一段脚本把所有fixture清点出来不需要太复杂正则匹配pytest.fixture和def中间的名字就行import ast from pathlib import Path source Path(tests/conftest.py).read_text(encodingutf-8) tree ast.parse(source) for node in tree.body: if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): decorators [d.id if isinstance(d, ast.Name) else for d in node.decorator_list] if fixture in decorators: scope for dec in node.decorator_list: if isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute): for kw in dec.keywords: if kw.arg scope: scope kw.value.value print(f{node.name}\tscope{scope}\tline{node.lineno})这段脚本只是抛砖引玉实际项目里你可能还要统计每个fixture的函数体行数、依赖的fixture名称通过解析函数参数。整理完之后把这些fixture按用途分成几类环境配置类读取环境变量、加载配置文件、临时目录、全局的ini参数。外部服务类数据库连接、Redis连接、MQ、第三方API客户端、浏览器实例。状态构造类造数据、构造订单、创建用户、生成token、打桩返回mock数据。报告与运行类接管pytest hook比如失败重试、展示优化、性能埋点。这个分类其实就是后续目录结构的雏形。大方向是环境配置类进fixtures/config.py外部服务类按服务拆分状态构造类按业务模块分组报告与运行类进hooks/。3.2 拆分动作一按领域模块迁移fixture分类完成之后就开始真正动手。我强烈建议按“每迁一个fixture就全量跑一遍相关用例”的节奏推进不要一次性把1200行全搬完再跑测试。一次性大迁移的问题是出错之后你根本不知道是哪个fixture导致的排查范围太大会让人直接心态爆炸。以数据库fixture为例。原来的代码大概是这样的# 重构前在tests/conftest.py中 import pytest from db import get_engine, create_session pytest.fixture(scopesession) def db_engine(): engine get_engine() yield engine engine.dispose() pytest.fixture() def db_session(db_engine): session create_session(db_engine) yield session session.close()迁移到tests/fixtures/db.py之后代码几乎可以原样保留唯一要注意的是如果这个模块里还要定义其他fixture这些fixture的依赖关系必须在本模块内清晰可见。也就是说db_session依赖db_engine两者都在同一个模块里这个模块自洽别人阅读这个文件就能把完整的依赖链看明白。# tests/fixtures/db.py import pytest from db import get_engine, create_session pytest.fixture(scopesession) def db_engine(): engine get_engine() yield engine engine.dispose() pytest.fixture() def db_session(db_engine): session create_session(db_engine) yield session session.close()然后顶层conftest.py里注册fixtures.db即可。这里有个细节值得强调迁移fixture时要连同它依赖的所有外部资源接口一起考虑。比如db_engine读取的是DATABASE_URL环境变量那这个读取动作是留在fixture内部还是放到一个专门的config模块里我倾向放到fixtures/config.py里定义一个app_configfixture再把配置值传给db_engine。这样做的原因是config模块可以作为叶子节点被所有其他模块依赖形成单向依赖链不会出现循环依赖。# tests/fixtures/config.py import os import pytest pytest.fixture(scopesession) def app_config(): return { database_url: os.getenv(DATABASE_URL, postgresql://localhost/testdb), api_base_url: os.getenv(API_BASE_URL, http://localhost:8080), timeout: int(os.getenv(HTTP_TIMEOUT, 30)), }# tests/fixtures/db.py 改造成这样 pytest.fixture(scopesession) def db_engine(app_config): engine get_engine(app_config[database_url]) yield engine engine.dispose()依赖关系变得显式哪个fixture需要什么配置一目了然。以后新增fixture时也只需要问自己这个东西依赖的配置从哪里来把这条线的数据流理清楚conftest.py就很难再变成一锅粥。3.3 拆分动作二hook函数独立管理hook函数是企业级conftest.py里容易被忽视的另一大块。我将它们单独规划到hooks/目录每个hook文件按职责组合不要一个文件放所有hook。这样做的原因很简单hook函数操作的往往是pytest内部对象item、config、call并且对执行顺序敏感把它们混在一起会导致排错时根本分不清是谁改了谁。举个例子如果测试团队需要统计失败用例并自动重试一次可以封装到hooks/retry.py# tests/hooks/retry.py import pytest pytest.hookimpl(tryfirstTrue) def pytest_runtest_makereport(item, call): 在用例执行结束后判断是否需要重试。 if call.when ! call: return if call.excinfo is not None and not hasattr(item, retried): item.retried True # 这里预留重试逻辑由外部runner统一调度再比如报告优化类hook比如给每个测试用例自动打上模块名称的标签# tests/hooks/report.py import pytest pytest.hookimpl(tryfirstTrue) def pytest_collection_modifyitems(config, items): 给收集到的每个用例自动添加所属模块的mark。 for item in items: module_name item.module.__name__.split(.)[-1] item.add_marker(getattr(pytest.mark, module_name))这些hook之所以要独立成模块是因为它们往往依赖pytest内部状态而且执行顺序敏感。和fixture混在一起会让阅读的人分不清“这个函数到底是普通工具还是pytest扩展点”。独立以后每个文件的顶部就能说明它的用途排错时也容易定位。比如pytest运行行为变得诡异就直接去hooks目录下对应的文件里看而不是在一个两千行的文件里反复搜索。有一点要特别注意hook函数在不同模块里定义时如果都设置了tryfirstTrue或trylastTrue执行的先后顺序会影响结果。遇到这类依赖顺序的hook建议在顶层conftest.py里统一显式调度或者干脆把顺序敏感的hook都放进同一个模块从上到下按顺序定义不要拆开。顺序这种东西跨文件管理几乎注定要出问题。3.4 拆分动作三配置与静态资源外移除fixture和hook之外conftest.py里还经常躺着两类东西静态常量和资源路径。比如各种超时时间、账号密码、URL前缀、模板路径这些其实都不该出现在测试代码里更不该散落在conftest.py的各处。重构时我会把这类内容分三种方式处理与pytest运行参数有关的配置写进pytest.ini或pyproject.toml的[tool.pytest.ini_options]下。与运行环境有关的变量统一从环境变量读取集中到一个config模块里管理。与业务相关的常量比如默认订单金额、默认用户名放到对应的领域模块目录下的constants.py而不是塞给全局conftest。举例说明# pytest.ini [pytest] markers smoke: 冒烟用例 slow: 慢速用例 order: 订单模块用例 addopts -ra -q --strict-markers这些标签和命令行参数移出去之后conftest.py彻底和“运行策略”解耦。如果后续要接CI流水线CI只需要修改ini文件里的参数或者通过命令行覆盖不需要去碰测试代码。这是一个非常容易被忽略的收益点很多项目直到接CI那天才发现conftest.py里的配置写得太死容器里一跑就各种水土不服。静态资源路径的迁移也同理。原来conftest.py里可能有个DATA_DIR Path(__file__).parent / data的常量它应该被放到一个专门的config模块中# tests/fixtures/config.py补充路径部分 from pathlib import Path PROJECT_ROOT Path(__file__).resolve().parents[2] TEST_DATA_DIR PROJECT_ROOT / tests / data这样所有需要读测试数据的fixture统一从这个模块取路径。重构之后数据的目录结构变化只需要改一个地方而不是全局搜索替换两三种写法不同的路径拼接。3.5 重构完成后的验证清单拆分动作全部做完以后我一般会走一遍验证清单逐项确认没有遗漏顶层conftest.py行数是否已经压到可维护范围我建议控制在100行以内。每一个领域fixture模块是否自洽模块内的fixture依赖关系清晰跨模块依赖只有config层或公共服务层。pytest --fixtures输出的fixture列表是否整洁名称无重复、无大量语义相近的名字。全量测试通过率是否和重构前一致如果重构前有失败用例重构后失败用例集合应该一致不能凭空多出一批新失败。随机抽几个用例手动验证会话级fixture的清理动作是否生效比如数据库连接确实被dispose了浏览器确实被关闭了。用pytest --collect-only -q确认测试收集没有遗漏或重复。我习惯在验证阶段顺手生成一份fixture清单快照存到docs里方便团队后续审查。以后任何人再往conftest.py里加fixture先对照快照看是否与已有fixture重复。这个动作虽然简单但对保持重构成果的作用相当明显。4. 重构过程中的常见问题与排查技巧4.1 fixture找不到或加载顺序异常fixtures.*模块注册了但运行测试时提示fixture xxx not found这是拆分后最常见的报错。我排查时按这个顺序走。先确认模块路径是否正确。pytest_plugins里写的是“模块导入路径”不是目录路径。假如tests/fixtures/db.py文件的包名叫fixtures.db并且tests/fixtures/目录下有__init__.py这个导入才是合法的。如果没有__init__.pypytest的导入机制虽然在某些版本下也能工作但强烈建议补上否则后续越发越乱。然后确认顶层conftest.py里的pytest_plugins列表是否被覆盖。有个很隐蔽的坑如果在conftest.py里把pytest_plugins放在某个函数内部赋值或者用变量名覆盖了它pytest根本不会读取。它必须是一个模块级别的名字且直接出现在conftest.py的顶层作用域。如果确认了路径没问题那就是加载顺序问题。pytest_plugins在列表中的顺序虽然大体决定了加载顺序但并不是绝对可靠的保障。当模块A的fixture依赖模块B的fixture而B又依赖A时就形成了循环导入。解决办法是打破循环把公共依赖抽到更底层比如都依赖config模块。循环依赖是设计问题靠调顺序解决不了只能从依赖图结构上动手。4.2 作用域膨胀导致测试串联这是重构后最容易暴露的存量问题。session级fixture太多的时候用例之间“隐形串数据”特别严重。最常见的是数据库session两个用例先后修改了同一行记录第二个用例断言失败可单独跑第二个用例又是通过的。出现这种“单独跑过、一起跑挂”的情况十有八九就是session级别共享状态在作祟。重构时遇到这类问题我的处理手法是把绝大多数数据库相关的fixture降级到function级session级只保留连接池或engine。真正的CRUD操作session每次用例新建用完即关。这样做的缺点是性能会略下降但换来的是隔离性。如果要兼顾性能可以在fixture里做“事务回滚”策略用例开始时开启事务结束后回滚不真正commit。# tests/fixtures/db.py import pytest pytest.fixture() def db_session(db_engine): 每个用例独立事务结束后回滚避免数据污染。 session create_session(db_engine) session.begin() yield session session.rollback() session.close()这个方案在单元测试和接口测试中都很实用。这里要注意如果代码内部自行commit了事务回滚就失效了需要配合嵌套事务或者让代码支持事务回调否则只能老老实实清理数据。遇到回滚失效时不要怀疑方案本身先检查业务代码里是不是把commit写死在了底层方法中。4.3 隐式依赖autouse滥用重构过程中我问得最多的一个问题“这个autouse的fixture到底对哪些用例生效”答案往往是没人说得清。我遇到过一个团队conftest.py里autouse的fixture有7个分别做了设置locale、设置环境变量、创建临时目录、mock时间、mock了远程调用、修改了log级别、还预先加载了大批测试数据。结果就是每个用例跑起来都很慢但谁都不敢动因为不知道删掉哪个会影响什么。我的建议是autouse只适用于那些“所有用例都无条件需要、且没有副作用”的全局准备动作。比如设置一个全局性的环境变量确保每个用例都跑在测试环境这就是合理的autouse。而mock远程调用、加载数据这类有业务语义的fixture应当显式地在用例参数里声明依赖让代码可读性更强。如果在重构时必须保留某个autouse fixture我建议在pytest_collection_modifyitems的hook里输出一份日志打印每个用例被哪些autouse fixture注入了至少在排查时有线索。手动搜索源码根本找不到引用关系因为你搜“tmp_path”会搜出一堆结果但你根本没法判断哪个用例是隐式依赖的哪个是显式声明的。4.4 多环境配置管理企业级测试还有一个绕不开的问题dev、staging、test多套环境。很多团队把所有环境的地址写成常量放在conftest.py顶部每次手工改。重构之后这部分被集中到config模块配合TEST_ENV变量切换。我常用的做法是用os.getenv加默认值同时加白名单校验# tests/fixtures/config.py import os ALLOWED_ENVS {dev, staging, test} pytest.fixture(scopesession) def app_config(): env os.getenv(TEST_ENV, dev) if env not in ALLOWED_ENVS: raise ValueError(f未知环境: {env}) return { env: env, database_url: os.getenv(fDATABASE_URL_{env.upper()}, postgresql://localhost/testdb), api_base_url: os.getenv(fAPI_BASE_URL_{env.upper()}, http://localhost:8080), }环境切换的入口收敛到一处CI里只需要注入TEST_ENV变量。这比在conftest.py里散落几十个地址常量要稳得多。还有一个附带的好处写文档的时候只需要说明一组环境变量的命名规则而不是把每个环境的地址列成一个超长表格。4.5 回归风险控制最后聊一下重构期间的回归风险。我的经验是所有前提中最重要的就是“小步走”。每次只迁一个fixture或者一个hook模块迁移完立刻跑这一模块相关的用例通过后再继续。不要试图用一整个周末把全量重构完周一直接提交一个大PR——出了问题光review就要一天情绪成本太高。另外重构前给tests目录做一个git分支跑一遍全量测试并记录失败清单。重构完成后对照这份清单确保新引入的失败用例为零。这个动作看起来简单但很多团队懒得做最后重构完上线发现一堆回归反而对“重构”这件事产生不信任。实际上大部分回归问题都是可以在小步迁移的过程中提前暴露的只是大家习惯性跳过了这一步。提示如果团队没有统一格式化工具先统一引入flake8/ruff/black再进行重构。混着不同风格挪代码后续diff查看会非常痛苦review的时候注意力都会被格式乱七八糟带跑。5. 写在最后重构之后怎么防止它再次腐烂到这里重构流程已经完整走过一遍。我想再分享一个实务层面的体会conftest.py之所以容易烂不是因为写代码的人水平差而是因为它天然就处在一个“极端方便”的位置——任何东西放进去都能被所有测试立刻使用这种即时满足感会让大家无意识地在里面堆垃圾。重构的作用是给这个文件立规矩但规矩立完还要有人维护。从个人经验来说重构完成后最有效的一个制度是“fixture新增提案制”。虽然听起来有点形式主义但至少要让团队约定新增fixture先看docs/fixtures.md清单确认没有现成的再决定是加到领域模块还是共享层。我还会在CI里加一个轻量检查如果顶层conftest.py行数超过150行直接让流水线亮黄牌警告。限制文件行数说白了很机械但对抑制膨胀确实立竿见影因为一旦超过阈值就会有人去问“这东西为什么在这里”。最后再分享一个操作性很强的小技巧重构后建议至少用一整周持续观察pytest的收集时间和单条用例耗时。你很快会发现某些“看起来做了一次、实际上根本没被用到”的session级fixture被删除后收集时间会明显缩短。如果发现重构后收集时间反而变长了优先检查是不是pytest_plugins列表里加载了过多不必要的模块精简掉那些只在少数用例中使用的共享模块改用业务模块目录下的局部conftest.py加载。
分享:

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

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