干测试这行尤其是天天跟接口自动化和UI自动化打交道的话“登录”这两个字估计早就刻进DNA里了。几乎所有用例都绕不开登录态很多岗位的实际需求就是登录逻辑跑一次后面一堆用例共享这个登录状态别每跑一条用例都重新登录一遍别因为登录模块的执行顺序导致整个项目的用例全部失败。Pytest新手往往卡住的点也就在这——怎么让登录先执行、怎么让别的模块能取到登录信息而不是傻乎乎地在每个.py文件里复制粘贴登录代码。这篇文章不绕弯子直接把我实际项目中验证过的一套方案写出来从fixture作用域到conftest组织方式再到常见坑的排查技巧一次性讲透。适合正在用pytest做接口自动化、或者想把登录态管理好好捋一遍的测试开发同学。1. 顺序依赖的本质别用“用例顺序”去解决用“fixture依赖”去解决1.1 为什么pytest不能靠“让登录模块先跑”来解决问题很多人一开始的思路是既然登录要先执行那我就把login用例放在第一个文件里或者在conftest里加一段“最先执行”的代码。这个思路其实从一开始就跑偏了。pytest收集用例的默认规则是测试文件按文件名字母顺序收集文件内按类定义顺序和函数定义顺序从上往下收集。你确实可以把测试文件命名为a_login_test.py让它排在前面但这是典型的碰运气做法。原因有两个。第一一旦用例数量上来或者后期有人加了新的测试文件英文字母排序会变登录文件可能不再是第一个。第二即使登录用例第一个执行了它产生的token怎么传给后面的用例通过模块级全局变量通过文件读写这些办法都很脆测试并发、xdist分片、pytest缓存都会把这些方案冲击得七零八落。所以我在项目里定的第一原则就是永远不要依赖收集顺序去保证执行依赖。顺序是pytest的收集策略依赖是你的业务约束。把两件事混为一谈后面排查问题的时候会非常痛苦。1.2 fixture才是pytest里处理“前置条件”的标准姿势Pytest解决这类问题的核心机制是fixture夹具。fixture本质上是一个带返回值或者带副作用的函数测试用例通过函数参数名去“声明”自己需要哪个fixture。当用例执行时pytest会先创建它依赖的所有fixture再执行用例本身。这个机制和unittest里的setUp有很大不同。setUp是面向类的同一个测试类里每个用例都跑一遍setUp而fixture是有作用域scope的可以做到整个测试会话只跑一次然后把返回值反复提供给多个用例。这一点本质上是“依赖注入”的思路——用例不关心登录动作在哪执行只关心在我需要的时候登录态已经被准备好了。先看个最简单的例子import pytest pytest.fixture(scopesession) def login_token(): # 这里做真实的登录操作返回token token fake-token-for-demo return token def test_order_list(login_token): headers {Authorization: fBearer {login_token}} # 带着headers请求业务接口 assert headers[Authorization] Bearer fake-token-for-demo这个例子中test_order_list并没有直接调用登录函数而是声明了自己需要login_token这个fixture。pytest发现这个依赖后会先去执行login_token()把返回值注入到测试函数里。这就是整个方案的基石。1.3 对比unittest的setUpClasspytest的优势到底在哪用unittest的老同学可能说setUpClass也能做到类级别只执行一次为什么非得用pytest的fixture区别其实不在“是否只执行一次”而在“依赖关系怎么表达”。setUpClass是隐式的子类继承一个父类父类的setUpClass干什么、什么时候被调用子类并不清楚。而且setUpClass是静态的只能操作self表达“这个测试模块需要登录态”这件事靠的是继承关系这个非常间接的方式。pytest的fixture是显式的声明式依赖。测试函数写的参数名就是它需要的依赖。你看到def test_create_order(login_token)这个签名一眼就知道它依赖登录态。更重要的是fixture可以组合A依赖BB依赖Cpytest会自动按依赖图去创建不需要你人工排序。这套机制处理复杂的测试前置条件比unittest的继承体系清晰太多。2. 登录fixture怎么写作用域、conftest与autouse的配合2.1 scope参数决定登录执行多少次登录状态共享这个需求最核心的一个参数就是scope。fixture的scope有四个常用值function、class、module、session。scopefunction每个测试函数执行前都跑一次这是默认值。登录代码写在这种fixture里等于每条用例都重新登录一遍虽然能工作但效率极低。scopeclass每个测试类跑一次类内所有用例共享。scopemodule每个测试模块跑一次文件内所有用例共享。scopesession整个测试会话只跑一次所有模块共享。这就是我们最想要的“登录一次全项目共享”。可以把fixture理解成一张缓存表pytest以fixture名为key、以scope为生命周期记录了创建结果。session级别的fixture第一次被某个用例触及时创建之后任何地方引用这个fixture直接返回第一次创建的对象不会重复执行函数体。实测中最直观的验证方式是用--setup-show参数跑一下。你能看到类似这样的输出SETUP S login_token my_test.py::test_create_order (fixtures used: login_token) TEARDOWN S login_token注意看SETUP S login_token只出现一次TEARDOWN S login_token在测试会话结束才出现一次。这意味着整个测试进程里登录代码只执行了一次token全程复用。我强烈建议所有刚接触fixture的人都跑一下--setup-show它比任何文档都能让人理解fixture机制。2.2 conftest.py把登录fixture放在所有测试模块都能找到的地方fixture可以定义在测试函数所在文件里也可以定义在conftest.py里。如果登录fixture只在某个测试文件里使用定义在同一个文件里没问题。但大多数情况下登录态是全项目的事情我建议直接放在conftest.py。conftest.py是pytest的固定配置文件它有几个重要特性它不需要被importpytest会自动读取它作用域是当前目录及所有子目录在根目录建一个conftest.py所有测试文件都能引用里面定义的fixture。所以我的项目目录通常长这样project_root/ ├── conftest.py ├── api_client.py └── tests/ ├── test_login.py ├── test_order.py └── test_user.py根目录conftest.py里放登录fixture和公共请求fixturetests/目录下的所有测试文件直接通过参数名引用。这样登录逻辑只有一个副本不会出现“每个测试文件各写了一版登录代码”的灾难。2.3 autouse参数登录fixture可自动应用但别滥用有时候你希望整个测试会话里的所有用例都自动带上登录态不用每个用例都去声明参数。这时候可以用autouseTrue。pytest.fixture(scopesession, autouseTrue) def login_autouse(): token do_login() yield {token: token}加了autouseTrue后当前作用域内的所有测试都会使用这个fixture即使函数签名里没有它。听起来很省事但我在实际项目中用得很少原因是很隐性如果所有用例都被autouse登录fixture拖住那你连“测试一个不需要登录的接口”的场景都绕不开登录排查问题时很难定位到底是哪个用例触发的登录。而且一旦autousefixture和显式依赖之间出现作用域不匹配报错信息非常绕。我的经验是当项目测试用例绝大多数都明确依赖登录态时用autouse是合理简化但当项目里有大量不需要登录的用例时还是老老实实显式声明依赖可读性和可维护性都更好。2.4 三种让登录“先执行”的方案对比严格来说pytest没有“先执行登录用例”这种原语但有以下三种成熟的“先执行登录fixture”的思路各有适用场景方案写法适用场景注意点显式依赖def test_a(login_token)所有需要登录态的用例最推荐依赖关系一目了然autousepytest.fixture(autouseTrue)绝大多数用例都要登录态会隐式作用于所有用例标记式依赖pytest.mark.usefixtures(login_token)想在类或模块级别批量声明拿不到fixture返回值只能当副作用pytest.mark.usefixtures这个方案值得展开说。它有自动使用的效果但又不像autouse一样对所有用例生效而是通过标记精确控制作用范围。但它有一个致命限制通过usefixtures使用fixture测试函数里无法直接获取fixture的返回值。要是你需要在用例里拿token拼header就不能只靠这个标记还需要在函数参数里再写一次。所以我的建议是usefixtures适合“只想让这个fixture执行一下副作用”的场景而不是“我要用它的返回值”的场景。3. 共享登录状态的实现方案从token到cookie再到requests.Session3.1 session级fixture返回token是最简单的状态共享登录成功后我们要共享的东西通常有三种token字符串、cookie、或者一个带登录态的HTTP客户端通常是requests.Session。最朴素也最常用的写法是让session级fixture直接返回token。import pytest import requests # 演示用的登录接口实际项目替换为真实端点和参数 def http_login(username: str, password: str) - str: resp requests.post( https://api.example.com/login, json{username: username, password: password}, timeout10 ) resp.raise_for_status() return resp.cookies if token not in resp.json() else resp.json()[token] pytest.fixture(scopesession) def login_token(): token http_login(tester01, s3cret) return token pytest.fixture(scopesession) def auth_headers(login_token): # 依赖login_token同时自己也是session级全项目共享一份headers return {Authorization: fBearer {login_token}}这里有个细节auth_headers也声明成scopesession它的执行次数同样是一次。但要注意pytest对session级fixture有个约束session级fixture不能依赖function级fixture。比如上面auth_headers依赖login_token两者都是session级这没问题但如果auth_headers里依赖了一个默认function级的fixturepytest会直接抛ScopeMismatch错误因为它不知道该怎么在一个session级的fixture里去创建一个function级的东西。3.2 进程内共享 vs 跨进程共享单进程模式下session级fixture的返回值存在内存里就够了。但一旦引入pytest-xdist做多进程并行测试情况就变了每个worker进程都是独立的解释器内存不互通session级fixture会在每个worker进程里各自执行一次等于登录操作被执行N次。针对这个问题通常有两个解法。第一个是尽量不用xdist的自动分片而是按模块分发pytest -n 4 --dist loadscope。loadscope模式下pytest会尽量把一个测试文件的所有用例分到同一个worker里这样登录fixture在同一个模块内只需要执行一次通过模块级共享完成。实际项目中我经常用这个模式简单、稳定、不会引入跨进程通信的复杂度。第二个是如果必须跨进程共享同一个登录态那就不能只靠fixture了。你需要一个外部存储比如把token写入一个临时文件或Redis用文件锁来保证只有一个worker执行登录其余worker读取已生成的token。这种方案在大型项目里确实存在但复杂度上升不少如果业务上完全没有要求“所有worker必须用同一个token”我建议先别上等真出现登录接口被压垮了再改造都来得及。在实际落地上我一般这么写一个简单的文件级token缓存import json import os import time TOKEN_FILE .test_token_cache.json pytest.fixture(scopesession) def login_token(tmp_path_factory): cache_path tmp_path_factory.mktemp(cache) / token.json if cache_path.exists(): data json.loads(cache_path.read_text()) if data.get(expires_at, 0) time.time(): return data[token] token http_login(tester01, s3cret) cache_path.write_text(json.dumps({ token: token, expires_at: time.time() 3600 })) return token注意tmp_path_factory是pytest内置的session级临时目录fixture不同worker之间其实是隔离的所以这个写法严格来说仍然是每worker一份token。如果真要跨worker共享得把缓存文件放到一个所有worker都能访问的固定路径上比如项目根目录再加一个锁。这块我在后文常见问题章节里再展开。3.3 用requests.Session共享cookie会话如果你的系统登录态主要是cookie而不是token那更推荐用requests.Session对象来做共享。requests.Session天然维护cookie、连接池和请求头登录后cookie就存在session对象里后续所有请求都用这同一个session理论上比手动取出cookie再拼接更接近真实浏览器的行为。import pytest import requests pytest.fixture(scopesession) def client(): session requests.Session() session.post( https://api.example.com/login, json{username: tester01, password: s3cret} ) return session def test_profile(client): resp client.get(https://api.example.com/profile) assert resp.status_code 200这个方案我更喜欢。因为业务接口的测试代码里不需要再写什么Authorization逻辑直接就是client.get(...)很像一个已经登录好的HTTP客户端。之后如果登录态从cookie换成了header token只需要改client这一个fixture内部的实现所有测试用例完全不需要动。这也是fixture封装状态的意义所在——把变化隔离在一个地方。3.4 fixture的teardown登录态的优雅释放登录fixture的释放时机也是有讲究的。pyest从3.0开始支持yield形式的fixture。函数写到yield之前的部分是setupyield之后的部分是teardown。对登录状态来说teardown通常是做登出操作、清理服务端会话、删除本地缓存文件之类的事情。pytest.fixture(scopesession) def client(): session requests.Session() session.post(https://api.example.com/login, json{username: tester01, password: s3cret}) yield session session.post(https://api.example.com/logout)这个teardown会在测试会话结束、所有依赖这个fixture的用例都已经跑完之后再执行并不占用用例时间。需要注意的是如果中间断言失败导致用例中断teardown也仍然会执行这是pytest保证的机制所以不用太担心资源没被释放。我在项目中还会在teardown里做一件事把登录失败的响应体完整打出来。这样一旦整个测试会话结束时发现teardown报错能快速判断是token被服务端置失效了还是网络问题。日志是排查分布式测试问题的最好抓手永远别心疼那几行print。4. 完整实操搭建一个共享登录态的pytest接口测试工程4.1 工程目录结构我这个示例会贴近真实项目包括一个假的登录接口、一个带token校验的业务接口、公共的conftest以及几个测试模块。demo_api_test/ ├── conftest.py ├── api_client.py ├── pytest.ini └── tests/ ├── test_login.py ├── test_order.py └── test_user.pypytest.ini里主要配置testpaths tests限定测试目录以及配置日志打印格式。如果项目里测试文件不在根目录这个文件能让pytest迅速完成配置减少启动阶段的额外开销。4.2 预置一个能跑起来的最小后端为了让示例可直接复现我用Flask写一个极简的服务端逻辑很简单/login返回一个token/orders校验Authorization头。# server.py from flask import Flask, request, jsonify app Flask(__name__) TOKENS {test-token-123} app.post(/login) def login(): data request.get_json() if data.get(username) tester01 and data.get(password) s3cret: return jsonify({token: test-token-123}) return jsonify({error: invalid credentials}), 401 app.get(/orders) def orders(): auth request.headers.get(Authorization, ) token auth.replace(Bearer , ) if token in TOKENS: return jsonify([{id: 1, name: test-order}]) return jsonify({error: unauthorized}), 401 if __name__ __main__: app.run(port5050, debugTrue)这个服务端示例里我没有做任何复杂的鉴权只是为了演示登录和共享登录态的主流程。真实项目中这层代码通常由开发团队提供不需要你单独去写。4.3 conftest.py全局登录fixture的实现在工程根目录建conftest.py里面定义登录fixture和基于登录态的公共请求fixture。这里我刻意用了scopesession并且把登录和请求封装拆开以便展示依赖链。# conftest.py import pytest import requests BASE_URL http://127.0.0.1:5050 pytest.fixture(scopesession) def login_token(): resp requests.post(f{BASE_URL}/login, json{username: tester01, password: s3cret}) assert resp.status_code 200, f登录失败status_code{resp.status_code}, body{resp.text} return resp.json()[token] pytest.fixture(scopesession) def auth_headers(login_token): return {Authorization: fBearer {login_token}}注意我把assert写在了fixture内部。这个设计有讲究如果登录接口挂了测试不会以一条“connect timeout”这种模糊的报错结束而是立刻抛出“登录失败status_code500”这种直接定位问题的信息。fixture内部的assert失败会导致所有依赖它的用例被置为error而不是逐个fail这样反而容易从测试报告里发现问题。4.4 各测试模块如何使用登录态测试文件里不必再关心token从哪来直接声明依赖即可。tests/test_login.py负责验证登录接口本身的行为。这里的用例全部显式依赖login_token但这个文件的目的完全是校验登录态的合法性# tests/test_login.py import pytest def test_login_token_not_empty(login_token): assert login_token test-token-123 def test_login_token_usable(login_token): # 模拟服务端校验token可用性真实项目中可以用一个轻量接口来测 assert len(login_token) 8tests/test_order.py模拟业务模块测试用例不直接依赖login_token而是依赖更上层的auth_headers。这就是“登录先执行”的最直观体现——login_token被auth_headers间接拉起来执行顺序由依赖关系自动决定。# tests/test_order.py import requests BASE_URL http://127.0.0.1:5050 def test_create_order(auth_headers): resp requests.get(f{BASE_URL}/orders, headersauth_headers) assert resp.status_code 200 assert resp.json()[0][name] test-order def test_unauthorized_request_returns_401(): # 不传headers时服务端应拒绝请求 resp requests.get(f{BASE_URL}/orders) assert resp.status_code 401第二个用例很有意思。它恰恰说明了“不需要登录的用例”也可以和“需要登录的用例”共存在同一个测试文件里互不干扰因为pytest只看你自己声明的依赖。4.5 运行观察如何确认登录只执行了一次在项目根目录运行pytest -v --setup-show你会看到类似下面的输出SETUP S login_token SETUP S auth_headers tests/test_order.py::test_create_order (fixtures used: auth_headers, login_token) SETUP S auth_headers tests/test_order.py::test_unauthorized_request_returns_401 (fixtures used: ) TEARDOWN S auth_headers TEARDOWN S login_token只看login_token这一行你会发现它只在最上面出现了一次SETUP S整个测试会话结束后才出现一次TEARDOWN S。这就是“登录模块先执行并共享登录状态”的铁证登录的动作只执行了一次但两个测试文件的所有用例都从它那里拿到了状态。如果想再验证一次可以在登录fixture里加一行print( 这里执行了真实的登录请求 )然后跑pytest -s -v。整个会话只打印这一行基本就可以确定方案没写错。我建议把这个print留到项目稳定后再删掉调试期它比任何日志都好用。5. 常见问题与排查技巧实录5.1 session级fixture被多次执行的排查思路有同学遇到过这种情况明明写了scopesession但控制台里登录逻辑还是执行了好几遍。遇到这个问题我会按以下顺序排查。第一确认是不是自己误用了scope参数。比如有人写的是pytest.fixture而不是pytest.fixture(scopesession)默认是function级每条用例都会触发一次登录。这个错误在代码审查中出现的频率非常高。第二确认是不是跑了多个pytest进程。如果你用pytest-xdist开了多个worker每个worker都是独立的解释器session级fixture在每一个worker里都会执行一次。这是正常现象不算bug解决方式就是我在前文提到的--dist loadscope策略或者接受“每个worker执行一次登录”这个事实。第三确认conftest有没有被重复加载。有些同学在多个子目录里各放了conftest.py里面都定义了同名fixture导致不同测试文件实际使用的是不同conftest里的不同fixture每个都执行一次登录。这种情况非常隐蔽因为测试报告上是看不到fixture对象差别的。我的排查技巧是在不同conftest的fixture里打印不同的日志内容跑一次立见分晓。5.2 token过期了怎么办要不要每次都重新登录token是有有效期的很多公司登录态可能只存活几十分钟。如果你session级fixture缓存的是登录后的token超过有效期后后面的用例会拿到一个失效的token接口返回401。这时候你面临的选择有两个。一个是定期刷新。在fixture内部记录获取token的时间每次使用时检查是否快过期如果快过期就重新登录。pytest.fixture(scopesession) def login_token(): token_cache {token: None, expires_at: 0} def _ensure_login(): if time.time() token_cache[expires_at] - 120: resp requests.post(...) token_cache[token] resp.json()[token] token_cache[expires_at] time.time() 3600 return token_cache[token] return _ensure_login这种情况下fixture返回的不再是一个固定token而是一个闭包函数每次使用都调用它来获取最新token。我倾向于在token有效期较短的项目里用这个方案因为它对测试用例来说完全无感该拿新token时自动就去拿了。另一个是干脆每次都重新登录但只影响涉及登录状态的用例。如果业务场景确实不强依赖token的复用那function级fixture反而更简单不涉及任何缓存和过期问题。这个选择取决于你们的登录接口扛不扛得住以及用例总量有多大。如果登录接口很贵、每个用例都登录会大大拖慢测试速度就用session级共享如果登录接口轻量function级也许比缓存过期问题更省心。5.3 登录失败时如何阻断后续用例登录fixture里需要放一个足够强硬的校验。如果登录接口返回非200后续接口用例再去请求业务接口得到的多半是一堆401或者超时不但无用还会把真实的业务bug淹没在海量无效报错里。我最常用的是在fixture里直接assert让关联用例整体变error。这样测试报告一眼就能看出是“环境问题/登录前置失败”而不是业务逻辑挂了。如果你希望更暴力一点在fixture里调用pytest.exit(登录失败终止本次测试会话)整个测试进程直接终止后面的用例全部不执行。这在冒烟测试场景里很管用但在全量回归里不建议用因为你会丢失其它与登录无关模块的测试结果。我实际项目中的习惯是冒烟场景用pytest.exit全量回归场景用assert这样既能在冒烟时快速止损又能保证回归时其它模块的覆盖率。5.4 多进程并发下的登录态共享方案使用pytest-xdist时session级fixture默认会在每个worker里分别执行一次。如果登录接口能够承受少量并发登录直接用-n auto --dist loadscope就够了既提升了整个回归的效率又因为按模块分发每个worker里每个模块的登录fixture一般只会执行一次成本可控。如果登录接口真的不能接受多次登录那就要引入跨进程状态存储了。我在一个项目里用过这样的方案把token写入临时文件登录前先检查文件是否存在且未过期存在就直接读取不存在就用一个文件锁锁住执行登录后写入文件。别的worker在锁外等待等当前worker释放锁后再读取文件。这个方案用filelock库能轻松实现。from filelock import FileLock pytest.fixture(scopesession) def login_token(tmp_path_factory): token_dir tmp_path_factory.mktemp(auth) token_file token_dir / token.json if token_file.exists(): data json.loads(token_file.read_text()) if data[expires_at] time.time(): return data[token] with FileLock(str(token_file) .lock): if token_file.exists(): data json.loads(token_file.read_text()) if data[expires_at] time.time(): return data[token] token do_login() token_file.write_text(json.dumps({token: token, expires_at: time.time() 3600})) return token注意tmp_path_factory在不同worker之间是不同目录所以这个方案真正落地时需要把token文件放到一个固定的共享目录里比如项目下的/tmp/auth_cache或/tmp/pytest_shared。文件锁本身是跨进程的不用担心两个worker同时登录。不过说实话凡是用到跨进程文件锁的场景测试基础设施的复杂度已经上了一个台阶。如果你所在的团队暂时没有“登录接口只能扛一次”这么极端的要求先用--dist loadscope基本能覆盖大部分问题。5.5 fixture的ScopeMismatch错误scope不匹配是登录fixture最常踩的坑。典型报错是ScopeMismatch: You tried to access the function scoped fixture xxx with a session scoped fixture yyy意思是session级fixture不能依赖function级fixture。为什么会这样因为session级fixture是在所有测试开始前创建的而function级fixture是每个测试执行前创建的。如果session级fixture里依赖了function级fixturepytest根本不知道该在哪个测试的上下文里创建它所以直接拒绝执行。解决思路是把依赖方向反过来让function级fixture依赖session级fixture这没问题或者把被依赖的fixture也改成session级。我在项目里见过一个反面教材登录fixture是session级的但登录时用的一个“随机用户名”fixture是function级的登录fixture一运行就报ScopeMismatch。改法也很简单把随机用户名fixture也声明成session级即可。5.6 调试登录和共享状态时值得遵循的几条经验回头总结下来我踩过的坑和沉淀下来的经验大部分可以用下面几条来概括给读者作为排查参考。第一条调试期间在fixture里留足够的日志。登录请求的URL、请求头、响应体、cookie全部打出来。很多人遇到登录失效问题查了半天代码都没问题最后一看是测试环境登录接口返回了一个新的跳转字段。有了完整日志这类问题三分钟就能定位没有日志可能消耗一上午。第二条把共享状态尽量封装在一个fixture里而不是分散在多个fixture里。如果你让每个测试模块各自定义自己的登录fixture然后期待它们之间共享同一个token那是不成立的。共享必须发生在公共的conftest里以session级fixture的方式存在。第三条使用--setup-show和--collect-only这两个辅助命令去观察fixture的setup和用例的收集情况。它们不能直接解决所有问题但能帮你快速确认“登录fixture到底被创建了几次”“哪些用例依赖了它”。第四条别在测试代码里对登录状态做硬编码。把token、用户名、密码放到配置文件或环境变量里通过fixture读取。测试项目一旦换环境跑硬编码的登录数据就是第一块多米诺骨牌。我习惯用pytest内置的--override-ini或者环境变量去传测试账号比如export TEST_USERNAMExxx这样每个环境的数据从部署配置里来测试代码本身不污染。最后再分享一个小技巧做登录共享主题的项目时我最后总会加一个“自检用例”在登录fixture里把token缓存到一个模块级变量然后写一个测试用例检查这个缓存变量的值确实被其它模块读取到了。这样做很傻但非常有效——它保证当有人重构conftest时如果fixture的scope被误改成function这个用例会立刻报警而不是等到所有业务用例开始大量报401时才反应过来。这个方法本质上是在测试“测试基础设施本身”。在自动化测试项目里测试框架代码和业务代码一样需要版本管理和回归别把它们当成一次性脚本。登录模块作为整个测试体系的地基越是基础越值得先写好、先验证好。