Pytest + Requests + YAML 数据驱动+日志模块

张开发
2026/4/3 5:06:50 15 分钟阅读
Pytest + Requests + YAML 数据驱动+日志模块
1. 写在前面这篇教程我不打算只讲“代码是什么意思”我会站在我自己搭一个接口自动化项目的角度把这套项目从头到尾串起来。我想做的不是“把几个 Python 文件拼起来跑通”而是把它整理成一套我以后遇到新系统时还能复用的思路。我这套项目现在验证的是一个典型的认证链路我先调用登录接口登录成功后拿到token我再带着token去获取用户信息然后我调用退出登录接口最后我再次访问用户信息接口验证旧token已经失效这个思路已经体现在我的配置文件、YAML 测试数据、公共工具封装、conftest.py的fixture设计以及 4 个测试文件的分工里。2. 我为什么要这样做这个项目我一开始学接口自动化的时候很容易陷入一个误区我只想着“怎么调一个接口”却没有去想“怎么把接口组织成一个项目”。所以这次我做这套项目时目标很明确我不只测一个登录接口我不只会写requests.post()我不只会写几个assert我要把测试数据、配置、公共方法、日志、前置依赖都拆开也就是说我想搭的是一个小型接口自动化项目骨架而不是零散脚本。在我的代码里这个骨架已经很清楚了配置在config.yml测试数据在 3 份 YAML 文件里请求和日志单独封装fixture统一放在conftest.py测试场景再拆分到不同的test_*.py文件里。3. 我先给自己画一个项目结构图从我现在这套代码的路径引用方式来看我心里的项目目录结构是这样的project/ ├─ common/ │ ├─ yaml_util.py │ ├─ request_util.py │ └─ logger_util.py ├─ config/ │ └─ config.yml ├─ data/ │ ├─ login.yml │ ├─ userinfo.yml │ └─ logout.yml └─ tests/ ├─ conftest.py ├─ test_login.py ├─ test_userinfo.py ├─ test_logout.py └─ test_auth_flow.py我之所以这样拆是因为我希望config目录只放环境配置data目录只放测试数据common目录只放公共工具tests目录只放测试逻辑这样分层之后我自己回头看代码时不会乱后面新增接口时也更容易扩展。这个思路已经在BASE_DIR、os.path.join(...)和各类 YAML 读取代码里体现出来了。4. 我先从配置文件开始理解整个项目4.1config.yml是我项目的环境入口我先看config.ymlbase_url:http://127.0.0.1:5000login_path:/api/loginuserinfo_path:/api/userinfologout_path:/api/logouttimeout:5这个文件虽然很小但它是我整个项目的“环境入口”。因为我后面所有测试最终都要依赖这几个值服务地址登录接口路径用户信息接口路径退出登录接口路径请求超时时间我这样做的好处很明显以后如果我把接口环境从本地地址换成测试环境地址我只需要改这个文件不用去每个测试文件里一个个改 URL。5. 我为什么把测试数据放进 YAML5.1 我不想把测试数据写死在 Python 里我以前初学的时候喜欢直接在测试函数里写usernameadminpassword123456这样写虽然快但后面测试数据一多代码就会越来越乱。所以我这次把测试数据单独放进 YAML 文件里。5.2login.yml我把登录场景拆成 4 组在login.yml里我定义了这几类场景正确登录密码错误用户名为空密码为空而且每条测试数据都被我拆成了titlerequestexpect我这样写不只是为了“看起来整齐”而是为了把一条测试用例拆成完整结构标题是什么请求传什么预期返回什么示例login_cases:-title:正确登录request:username:adminpassword:123456expect:code:200msg:登录成功token_not_null:true这一条就完整表达了一个登录成功场景。后面的密码错误、用户名为空、密码为空也是同样结构。5.3userinfo.yml我把成功场景和异常场景分开在用户信息接口里我没有只写“正常查到用户信息”这一条。我把它分成了两部分第一部分是成功场景已登录后获取用户信息第二部分是失败场景token 为空token 错误我这样拆是因为用户信息接口本质上就是一个需要认证的接口所以我不仅要验证“带对 token 时能不能成功”也要验证“token 错的时候系统能不能正确拦截”。5.4logout.yml退出登录我也测成功和失败退出登录也是同样思路。我没有只测“退出成功”还测了退出时 token 为空退出时 token 错误因为在我看来退出登录本质上也是认证校验的一部分。既然它依赖Authorization头那我就必须测它的正常输入和异常输入。6. 我是怎么读取 YAML 的6.1yaml_util.py很小但作用很大我的yaml_util.py只有一个函数importyamldefread_yaml(path):withopen(path,r,encodingutf-8)asf:returnyaml.safe_load(f)这个工具我故意写得很简单因为我只想让它做好一件事稳定地把 YAML 文件读成 Python 对象。这里我特别注意了两个点第一我明确指定了encodingutf-8。因为我这个项目里有中文标题、中文提示语如果不写编码Windows 下很容易遇到乱码或者读取报错。第二我使用的是yaml.safe_load()。我现在这个项目只是在读配置和测试数据不需要复杂对象构造所以用这种方式最稳。7. 我为什么把请求封装进request_util.py7.1 我不想每个测试都自己写一遍请求代码如果我在每个测试函数里都这样写responserequests.post(...)print(response.status_code)print(response.text)那测试文件会很快变得臃肿。所以我把请求统一封装进了request_util.py。7.2send_post()和send_get()不只是发请求我在request_util.py里封装了两个方法send_post()send_get()而且我在请求前后都加了日志请求 URL请求头请求参数或请求体响应状态码响应内容比如send_post()里我会先记录“我要请求哪个 URL”再打印请求头和 JSON等返回后再记录状态码和响应内容。这件事对我来说特别重要因为接口自动化最怕的一类问题就是我不知道请求到底发到了哪里我不知道请求体到底传了什么我不知道服务端到底回了什么有了统一请求封装我以后排查问题时就不用到处打print()了。8. 我为什么还要单独封装日志8.1logger_util.py是为了让调试更可控我的logger_util.py做了几件我认为非常实用的事如果log文件夹不存在就自动创建日志同时输出到控制台和日志文件日志里带时间、文件名、行号、级别如果 logger 已经有 handler就不重复添加这最后一点我很重视因为很多新手项目会出现一个问题日志重复打印。同一条日志打印两遍、三遍看起来非常乱。我这里通过if not self.logger.handlers:做了保护。所以我后面在request_util.py和conftest.py里都直接复用了这个 logger。9. 我怎么理解conftest.py9.1 在我眼里conftest.py就是测试项目的调度中心如果说 YAML 是“数据中心”那conftest.py对我来说就是“调度中心”。因为我在这里统一管理了配置文件读取URL 拼接超时时间各类测试数据读取登录 token专属 fresh token这样测试文件就只关心“测什么”而不用关心“这些公共资源怎么来”。9.2 我先用 fixture 读取配置和拼 URL在conftest.py里我先定义了这些session级别的 fixtureconfig_database_urllogin_urluserinfo_urllogout_urltimeout这样我在测试函数里就可以直接写deftest_xxx(login_url,timeout):而不用自己重复拼地址。9.3 我还把 YAML 数据读取也集中到了这里除了配置我还在conftest.py里定义了读取测试数据的 fixturelogin_casesuserinfo_casesinvalid_token_caseslogout_casesinvalid_logout_cases这样设计的好处是以后如果我换了 YAML 文件路径或者增加一层数据处理逻辑我只需要改conftest.py而不是每个测试文件都改。9.4login_token和fresh_login_token是我这套项目里最关键的设计之一这两个 fixture我觉得是整套项目最值得学习的地方。login_token我把它定义成了session级别。意思是整个测试会话里只登录一次拿到一个全局复用 token后面普通接口测试都可以复用它。fresh_login_token我把它定义成了function级别。意思是每个测试函数都重新登录一次拿一个专属 token避免测试之间互相影响。为什么我要这么设计因为我已经意识到一个很典型的问题有些测试会“消耗”登录态。比如退出登录测试。如果我让它用全局共享的login_token去退出那后面的用户信息测试就会全部失效。所以我必须给这类测试一个“独立 token”。这就是我从“能跑通”开始往“测试隔离”升级的关键一步。10. 我是怎么写登录测试的10.1test_login.py的核心就是参数化登录测试里我最核心的一句代码是pytest.mark.parametrize(case,_temp_cases,idsbuild_ids(_temp_cases))这句代码的意思是我让同一个test_login()函数拿着不同的测试数据反复执行。而测试名字则来自每个 case 的title。10.2 我在登录测试里做了什么在test_login()里我的思路很固定从 case 里拿到请求数据发登录请求解析响应 JSON断言code断言msg根据token_not_null判断 token 是否应该为空代码逻辑其实很朴素req_datacase[request]expect_datacase[expect]responsesend_post(urllogin_url,json_datareq_data,timeouttimeout)resultresponse.json()assertresult[code]expect_data[code]assertresult[msg]expect_data[msg]如果预期要求 token 非空我就断言它不是None否则我就断言它是None。这让我觉得登录测试不只是“成功登录”还同时覆盖了异常分支。11. 我是怎么写用户信息测试的11.1 用户信息测试的关键不是 GET而是“依赖登录态”在test_userinfo.py里我写了两类测试。第一类是成功场景deftest_userinfo_after_login(case,userinfo_url,timeout,login_token):这里最关键的不是userinfo_url而是login_token。因为这说明我在测试用户信息接口时已经明确意识到这个接口不是独立的它依赖前置登录状态。11.2 我在成功场景里验证了什么成功场景里我把login_token放进请求头headers{Authorization:login_token}然后调用send_get()去访问用户信息接口。接着我不仅断言codemsg还断言了usernamenameagerole这说明在我写这个测试时我已经不是只在测“能不能访问成功”而是在测返回的业务数据是不是正确。11.3 我在异常场景里测了什么异常部分我测了两种情况token 为空token 错误这里我没有复用login_token而是直接从 YAML 里读错误请求头去测试。这让我能验证系统在认证失败时是否返回了我预期的401或403。12. 我是怎么写退出登录测试的12.1 退出登录测试最大的重点是避免污染别的测试在test_logout.py里成功场景是这样定义的deftest_logout_success(case,logout_url,timeout,fresh_login_token):这里我特意没有用login_token而是用fresh_login_token。这背后其实体现的是我的一个测试设计原则会改变全局状态的测试尽量不要使用共享资源。因为退出登录会导致 token 失效所以它天然是一个“带副作用”的操作。我不希望它把别的测试搞崩所以我让它每次都单独拿一个 fresh token。12.2 退出登录异常场景我也没有省略在test_logout_invalid_token()里我同样覆盖了token 为空token 错误这样我就能验证退出登录接口在鉴权失败时的行为是否符合预期。13. 我最重视的一份测试认证闭环测试13.1test_auth_flow.py测的不是一个接口而是一整条链路在我这套项目里test_auth_flow.py是我最重视的一份测试。因为它不只是单点测试而是完整验证登录成功获取用户信息成功退出登录成功旧 token 再访问失败它的测试函数名字其实已经说明了一切deftest_login_userinfo_logout_userinfo_fail(...)这就是一条完整的认证闭环。13.2 我怎么理解这个闭环我把这个过程画成流程图会更直观startuml start :我先登录; if (登录成功) then (是) :我保存 token; :我去获取用户信息; if (获取成功) then (是) :我调用退出登录; if (退出成功) then (是) :我再次请求用户信息; :我断言 token 已失效; endif endif endif stop enduml这条链路对我来说最有价值的点在于我不是只测“退出登录接口返回成功”而是继续验证它到底有没有真的让 token 失效。因为很多系统会出现一种假象接口返回“退出成功”但实际上旧 token 还能继续访问如果我只测退出接口的返回值我就发现不了这个问题。14. 我从这套项目里学到的测试思维做完这套项目后我对接口自动化的理解不再是“写几个请求和断言”。我更清楚地意识到接口自动化项目至少要有下面这些思维。14.1 我要同时测正常和异常登录不是只测成功登录。用户信息不是只测能查到数据。退出登录也不是只测退出成功。每个接口我都要问自己正常时会怎样参数错误时会怎样认证失败时会怎样这一点在 3 份 YAML 和 3 个单接口测试文件里都体现得很明显。14.2 我要既测单接口也测业务链路单接口测试解决的是这个接口自己有没有问题闭环链路测试解决的是前后接口联动时有没有问题状态变化有没有问题前一个接口的结果能不能正确支撑后一个接口这就是为什么我不仅保留了登录、用户信息、退出登录的单独测试还写了一份test_auth_flow.py。14.3 我要区分共享资源和隔离资源以前我会觉得“拿个 token 用就行了”。但现在我已经知道测试设计里一个很重要的问题是这个资源应该共享还是应该隔离普通测试适合共享 token提高效率会改状态的测试适合独立 token避免互相污染这就是login_token和fresh_login_token的意义。15. 如果我来运行这套项目我会怎么做我会按下面这个顺序执行第一步先保证后端服务已经启动因为我在config.yml里配置的地址是base_url:http://127.0.0.1:5000所以我必须先保证本地服务跑在这个地址上并且有/api/login/api/userinfo/api/logout这三个接口。第二步进入项目根目录这一步很重要。尤其是我的test_login.py里有一行_temp_casesread_yaml(data/login.yml)[login_cases]这要求我运行时当前目录就是项目根目录否则会有路径问题。第三步执行 pytest我会用这种方式运行python-mpytest我更喜欢这样运行因为它通常比直接敲pytest更稳一些。第四步看控制台和日志文件我会同时看两处终端输出log目录下的日志文件因为我的 logger 已经把日志同时输出到了控制台和文件。这样排查问题更方便。16. 我自己会重点排查哪些常见问题16.1 路径问题这个是我现在这套代码里最容易踩的坑之一。因为test_login.py里直接写了read_yaml(data/login.yml)如果我不是在项目根目录执行 pytest就可能找不到文件。相比之下test_userinfo.py和test_logout.py里是通过BASE_DIR os.path.join(...)来读文件这种方式更稳。16.2 token 污染问题这个问题我现在已经比较敏感了。如果退出登录测试误用了共享 token那么后面依赖登录态的测试很可能会全部失败。所以我以后看到“会改状态”的测试时第一反应就会是这个测试会不会污染别的测试16.3 日志重复问题如果我以后扩展项目时发现同一条日志输出了多次我会第一时间去看 logger 有没有重复添加 handler。好在我现在的logger_util.py已经通过if not self.logger.handlers:做了保护。17. 如果我要按学习顺序消化这套项目我会怎么学如果让我重新从头学一遍这套项目我不会一上来就啃所有文件。我会按下面这个顺序来第一步先看config.yml因为我要先知道我的项目到底请求哪些接口、访问哪个地址、超时时间是多少。第二步再看 3 份 YAML 数据我要先明白每个接口都测了哪些场景正常和异常怎么拆。第三步再看yaml_util.py、request_util.py、logger_util.py因为我要先理解这套项目是怎么读数据、怎么发请求、怎么记日志的。第四步重点看conftest.py这里是整个项目的“资源调度中心”。我要真正理解 fixture、token、共享和隔离资源。第五步最后再看 4 个测试文件到这一步我再去理解每个测试场景就会轻松很多。因为我已经知道配置从哪里来、数据从哪里来、请求怎么发、日志怎么打、token 怎么拿。18. 我会怎么总结这套项目如果让我用一句话总结这套项目我会这样说我用 YAML 管测试数据用conftest.py管测试资源用requests封装请求用日志封装调试过程再用 pytest 把单接口测试和认证闭环测试串成一个小型接口自动化项目。19. 我下一步会怎么继续扩展如果我要继续把这套项目往下做我会优先考虑下面几个方向19.1 加一个“修改密码”接口因为这个接口能继续练状态变化前后依赖数据回写19.2 加一个“退出后重新登录”场景这样我就能更完整地验证 token 生命周期。19.3 把断言和报告再封装一下比如后面我可能会增加统一断言工具Allure 报告更规范的目录结构不过在我看来这些都属于下一阶段。当前这套项目最核心的价值还是让我真正理解了一个接口自动化项目不只是发请求而是要把配置、数据、资源、日志、场景和链路都组织起来。20. 最后我给自己的一个提醒做完这套项目后我会提醒自己一件事我以后看到一个系统不要只想着怎么调接口而要先想清楚这个接口属于哪条业务链路它依赖什么状态它会不会影响别的测试它的正常和异常场景分别是什么。只要我能一直这样去想我做的就不再是“零散测试脚本”而是在逐步形成自己的测试思维。

更多文章