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

1. 写在前面

这篇教程,我不打算只讲"代码是什么意思",我会站在我自己搭一个接口自动化项目 的角度,把这套项目从头到尾串起来。

我想做的不是"把几个 Python 文件拼起来跑通",而是把它整理成一套我以后遇到新系统时还能复用的思路

我这套项目现在验证的是一个典型的认证链路:

  1. 我先调用登录接口
  2. 登录成功后拿到 token
  3. 我再带着 token 去获取用户信息
  4. 然后我调用退出登录接口
  5. 最后我再次访问用户信息接口,验证旧 token 已经失效

这个思路,已经体现在我的配置文件、YAML 测试数据、公共工具封装、conftest.pyfixture 设计,以及 4 个测试文件的分工里。


2. 我为什么要这样做这个项目

我一开始学接口自动化的时候,很容易陷入一个误区:

我只想着"怎么调一个接口",却没有去想"怎么把接口组织成一个项目"。

所以这次我做这套项目时,目标很明确:

  • 我不只测一个登录接口
  • 我不只会写 requests.post()
  • 我不只会写几个 assert
  • 我要把测试数据、配置、公共方法、日志、前置依赖都拆开

也就是说,我想搭的是一个小型接口自动化项目骨架 ,而不是零散脚本。

在我的代码里,这个骨架已经很清楚了:配置在 config.yml,测试数据在 3 份 YAML 文件里,请求和日志单独封装,fixture 统一放在 conftest.py,测试场景再拆分到不同的 test_*.py 文件里。


3. 我先给自己画一个项目结构图

从我现在这套代码的路径引用方式来看,我心里的项目目录结构是这样的:

text 复制代码
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_DIRos.path.join(...) 和各类 YAML 读取代码里体现出来了。


4. 我先从配置文件开始理解整个项目

4.1 config.yml 是我项目的环境入口

我先看 config.yml

yaml 复制代码
base_url: "http://127.0.0.1:5000"
login_path: "/api/login"
userinfo_path: "/api/userinfo"
logout_path: "/api/logout"
timeout: 5

这个文件虽然很小,但它是我整个项目的"环境入口"。

因为我后面所有测试,最终都要依赖这几个值:

  • 服务地址
  • 登录接口路径
  • 用户信息接口路径
  • 退出登录接口路径
  • 请求超时时间

我这样做的好处很明显:

以后如果我把接口环境从本地地址换成测试环境地址,我只需要改这个文件,不用去每个测试文件里一个个改 URL。


5. 我为什么把测试数据放进 YAML

5.1 我不想把测试数据写死在 Python 里

我以前初学的时候,喜欢直接在测试函数里写:

python 复制代码
username = "admin"
password = "123456"

这样写虽然快,但后面测试数据一多,代码就会越来越乱。

所以我这次把测试数据单独放进 YAML 文件里。


5.2 login.yml:我把登录场景拆成 4 组

login.yml 里,我定义了这几类场景:

  • 正确登录
  • 密码错误
  • 用户名为空
  • 密码为空

而且每条测试数据都被我拆成了:

  • title
  • request
  • expect

我这样写,不只是为了"看起来整齐",而是为了把一条测试用例拆成完整结构:

  • 标题是什么
  • 请求传什么
  • 预期返回什么

示例:

yaml 复制代码
login_cases:
  - title: "正确登录"
    request:
      username: "admin"
      password: "123456"
    expect:
      code: 200
      msg: "登录成功"
      token_not_null: true

这一条就完整表达了一个登录成功场景。后面的密码错误、用户名为空、密码为空,也是同样结构。


5.3 userinfo.yml:我把成功场景和异常场景分开

在用户信息接口里,我没有只写"正常查到用户信息"这一条。

我把它分成了两部分:

第一部分是成功场景:

  • 已登录后获取用户信息

第二部分是失败场景:

  • token 为空
  • token 错误

我这样拆,是因为用户信息接口本质上就是一个需要认证的接口,所以我不仅要验证"带对 token 时能不能成功",也要验证"token 错的时候系统能不能正确拦截"。


5.4 logout.yml:退出登录我也测成功和失败

退出登录也是同样思路。

我没有只测"退出成功",还测了:

  • 退出时 token 为空
  • 退出时 token 错误

因为在我看来,退出登录本质上也是认证校验的一部分。

既然它依赖 Authorization 头,那我就必须测它的正常输入和异常输入。


6. 我是怎么读取 YAML 的

6.1 yaml_util.py 很小,但作用很大

我的 yaml_util.py 只有一个函数:

python 复制代码
import yaml

def read_yaml(path):
    with open(path, "r", encoding="utf-8") as f:
        return yaml.safe_load(f)

这个工具我故意写得很简单,因为我只想让它做好一件事:
稳定地把 YAML 文件读成 Python 对象。

这里我特别注意了两个点:

第一,我明确指定了 encoding="utf-8"

因为我这个项目里有中文标题、中文提示语,如果不写编码,Windows 下很容易遇到乱码或者读取报错。

第二,我使用的是 yaml.safe_load()

我现在这个项目只是在读配置和测试数据,不需要复杂对象构造,所以用这种方式最稳。


7. 我为什么把请求封装进 request_util.py

7.1 我不想每个测试都自己写一遍请求代码

如果我在每个测试函数里都这样写:

python 复制代码
response = requests.post(...)
print(response.status_code)
print(response.text)

那测试文件会很快变得臃肿。

所以我把请求统一封装进了 request_util.py


7.2 send_post()send_get() 不只是发请求

我在 request_util.py 里封装了两个方法:

  • send_post()
  • send_get()

而且我在请求前后都加了日志:

  • 请求 URL
  • 请求头
  • 请求参数或请求体
  • 响应状态码
  • 响应内容

比如 send_post() 里,我会先记录"我要请求哪个 URL",再打印请求头和 JSON,等返回后再记录状态码和响应内容。

这件事对我来说特别重要,因为接口自动化最怕的一类问题就是:

  • 我不知道请求到底发到了哪里
  • 我不知道请求体到底传了什么
  • 我不知道服务端到底回了什么

有了统一请求封装,我以后排查问题时就不用到处打 print() 了。


8. 我为什么还要单独封装日志

8.1 logger_util.py 是为了让调试更可控

我的 logger_util.py 做了几件我认为非常实用的事:

  • 如果 log 文件夹不存在,就自动创建
  • 日志同时输出到控制台和日志文件
  • 日志里带时间、文件名、行号、级别
  • 如果 logger 已经有 handler,就不重复添加

这最后一点我很重视,因为很多新手项目会出现一个问题:
日志重复打印 。同一条日志打印两遍、三遍,看起来非常乱。

我这里通过 if not self.logger.handlers: 做了保护。

所以我后面在 request_util.pyconftest.py 里,都直接复用了这个 logger。


9. 我怎么理解 conftest.py

9.1 在我眼里,conftest.py 就是测试项目的调度中心

如果说 YAML 是"数据中心",那 conftest.py 对我来说就是"调度中心"。

因为我在这里统一管理了:

  • 配置文件读取
  • URL 拼接
  • 超时时间
  • 各类测试数据读取
  • 登录 token
  • 专属 fresh token

这样,测试文件就只关心"测什么",而不用关心"这些公共资源怎么来"。


9.2 我先用 fixture 读取配置和拼 URL

conftest.py 里,我先定义了这些 session 级别的 fixture:

  • config_data
  • base_url
  • login_url
  • userinfo_url
  • logout_url
  • timeout

这样我在测试函数里就可以直接写:

python 复制代码
def test_xxx(login_url, timeout):

而不用自己重复拼地址。


9.3 我还把 YAML 数据读取也集中到了这里

除了配置,我还在 conftest.py 里定义了读取测试数据的 fixture:

  • login_cases
  • userinfo_cases
  • invalid_token_cases
  • logout_cases
  • invalid_logout_cases

这样设计的好处是:

以后如果我换了 YAML 文件路径,或者增加一层数据处理逻辑,我只需要改 conftest.py,而不是每个测试文件都改。


9.4 login_tokenfresh_login_token 是我这套项目里最关键的设计之一

这两个 fixture,我觉得是整套项目最值得学习的地方。

login_token

我把它定义成了 session 级别。

意思是:整个测试会话里,只登录一次,拿到一个全局复用 token,后面普通接口测试都可以复用它。

fresh_login_token

我把它定义成了 function 级别。

意思是:每个测试函数都重新登录一次,拿一个专属 token,避免测试之间互相影响。

为什么我要这么设计?

因为我已经意识到一个很典型的问题:
有些测试会"消耗"登录态。

比如退出登录测试。

如果我让它用全局共享的 login_token 去退出,那后面的用户信息测试就会全部失效。

所以我必须给这类测试一个"独立 token"。

这就是我从"能跑通"开始往"测试隔离"升级的关键一步。


10. 我是怎么写登录测试的

10.1 test_login.py 的核心就是参数化

登录测试里,我最核心的一句代码是:

python 复制代码
@pytest.mark.parametrize("case", _temp_cases, ids=build_ids(_temp_cases))

这句代码的意思是:

我让同一个 test_login() 函数,拿着不同的测试数据反复执行。

而测试名字则来自每个 case 的 title


10.2 我在登录测试里做了什么

test_login() 里,我的思路很固定:

  1. 从 case 里拿到请求数据
  2. 发登录请求
  3. 解析响应 JSON
  4. 断言 code
  5. 断言 msg
  6. 根据 token_not_null 判断 token 是否应该为空

代码逻辑其实很朴素:

python 复制代码
req_data = case["request"]
expect_data = case["expect"]

response = send_post(
    url=login_url,
    json_data=req_data,
    timeout=timeout
)

result = response.json()

assert result["code"] == expect_data["code"]
assert result["msg"] == expect_data["msg"]

如果预期要求 token 非空,我就断言它不是 None;否则我就断言它是 None

这让我觉得,登录测试不只是"成功登录",还同时覆盖了异常分支。


11. 我是怎么写用户信息测试的

11.1 用户信息测试的关键,不是 GET,而是"依赖登录态"

test_userinfo.py 里,我写了两类测试。

第一类是成功场景:

python 复制代码
def test_userinfo_after_login(case, userinfo_url, timeout, login_token):

这里最关键的不是 userinfo_url,而是 login_token

因为这说明我在测试用户信息接口时,已经明确意识到:

这个接口不是独立的,它依赖前置登录状态。


11.2 我在成功场景里验证了什么

成功场景里,我把 login_token 放进请求头:

python 复制代码
headers = {
    "Authorization": login_token
}

然后调用 send_get() 去访问用户信息接口。

接着我不仅断言:

  • code
  • msg

还断言了:

  • username
  • name
  • age
  • role

这说明在我写这个测试时,我已经不是只在测"能不能访问成功",而是在测:

返回的业务数据是不是正确。


11.3 我在异常场景里测了什么

异常部分我测了两种情况:

  • token 为空
  • token 错误

这里我没有复用 login_token,而是直接从 YAML 里读错误请求头去测试。

这让我能验证系统在认证失败时,是否返回了我预期的 401403


12. 我是怎么写退出登录测试的

12.1 退出登录测试最大的重点,是避免污染别的测试

test_logout.py 里,成功场景是这样定义的:

python 复制代码
def test_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.1 test_auth_flow.py 测的不是一个接口,而是一整条链路

在我这套项目里,test_auth_flow.py 是我最重视的一份测试。

因为它不只是单点测试,而是完整验证:

  • 登录成功
  • 获取用户信息成功
  • 退出登录成功
  • 旧 token 再访问失败

它的测试函数名字其实已经说明了一切:

python 复制代码
def test_login_userinfo_logout_userinfo_fail(...)

这就是一条完整的认证闭环。


13.2 我怎么理解这个闭环

我把这个过程画成流程图,会更直观:

plantuml 复制代码
@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_tokenfresh_login_token 的意义。


15. 如果我来运行这套项目,我会怎么做

我会按下面这个顺序执行:

第一步:先保证后端服务已经启动

因为我在 config.yml 里配置的地址是:

yaml 复制代码
base_url: "http://127.0.0.1:5000"

所以我必须先保证本地服务跑在这个地址上,并且有:

  • /api/login
  • /api/userinfo
  • /api/logout

这三个接口。


第二步:进入项目根目录

这一步很重要。

尤其是我的 test_login.py 里有一行:

python 复制代码
_temp_cases = read_yaml("data/login.yml")["login_cases"]

这要求我运行时当前目录就是项目根目录,否则会有路径问题。


第三步:执行 pytest

我会用这种方式运行:

bash 复制代码
python -m pytest

我更喜欢这样运行,因为它通常比直接敲 pytest 更稳一些。


第四步:看控制台和日志文件

我会同时看两处:

  • 终端输出
  • log 目录下的日志文件

因为我的 logger 已经把日志同时输出到了控制台和文件。这样排查问题更方便。


16. 我自己会重点排查哪些常见问题

16.1 路径问题

这个是我现在这套代码里最容易踩的坑之一。

因为 test_login.py 里直接写了:

python 复制代码
read_yaml("data/login.yml")

如果我不是在项目根目录执行 pytest,就可能找不到文件。

相比之下,test_userinfo.pytest_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.pyrequest_util.pylogger_util.py

因为我要先理解这套项目是怎么读数据、怎么发请求、怎么记日志的。

第四步:重点看 conftest.py

这里是整个项目的"资源调度中心"。

我要真正理解 fixture、token、共享和隔离资源。

第五步:最后再看 4 个测试文件

到这一步,我再去理解每个测试场景,就会轻松很多。

因为我已经知道配置从哪里来、数据从哪里来、请求怎么发、日志怎么打、token 怎么拿。


18. 我会怎么总结这套项目

如果让我用一句话总结这套项目,我会这样说:

我用 YAML 管测试数据,用 conftest.py 管测试资源,用 requests 封装请求,用日志封装调试过程,再用 pytest 把单接口测试和认证闭环测试串成一个小型接口自动化项目。


19. 我下一步会怎么继续扩展

如果我要继续把这套项目往下做,我会优先考虑下面几个方向:

19.1 加一个"修改密码"接口

因为这个接口能继续练:

  • 状态变化
  • 前后依赖
  • 数据回写

19.2 加一个"退出后重新登录"场景

这样我就能更完整地验证 token 生命周期。

19.3 把断言和报告再封装一下

比如后面我可能会增加:

  • 统一断言工具
  • Allure 报告
  • 更规范的目录结构

不过在我看来,这些都属于下一阶段。

当前这套项目最核心的价值,还是让我真正理解了:

一个接口自动化项目,不只是发请求,而是要把配置、数据、资源、日志、场景和链路都组织起来。


20. 最后,我给自己的一个提醒

做完这套项目后,我会提醒自己一件事:

我以后看到一个系统,不要只想着怎么调接口,而要先想清楚:这个接口属于哪条业务链路,它依赖什么状态,它会不会影响别的测试,它的正常和异常场景分别是什么。

只要我能一直这样去想,我做的就不再是"零散测试脚本",而是在逐步形成自己的测试思维。

相关推荐
北冥有羽Victoria2 小时前
Django 实战:SQLite 转 MySQL 与 Bootstrap 集成
大数据·服务器·python·django·编辑器
AI自动化工坊2 小时前
微软Agent Framework实战指南:统一Python和.NET的AI开发体验
人工智能·python·microsoft·.net·agent
林姜泽樾2 小时前
Python爬虫基础第一章,JSON
爬虫·python·网络爬虫
上海云盾-小余2 小时前
高防 IP 与高防 CDN 如何搭配使用?攻防效率最大化实战指南
网络·网络协议·tcp/ip
区块block2 小时前
连接人类算力与 AI 智能体:Aethr Protocol 正为 Web4.0 构建“能源网络”
网络·人工智能·能源
HalvmånEver2 小时前
Linux:基于TCP Socket的客户端-服务器实现的远程命令行项目
linux·运维·服务器·网络·tcp/ip
zzwq.2 小时前
深入理解Python闭包与装饰器:从入门到进阶
开发语言·python
网易独家音乐人Mike Zhou2 小时前
【Python】TXT、BIN文件的十六进制相互转换小程序
python·单片机·mcu·小程序·嵌入式·ti毫米波雷达
24zhgjx-fuhao2 小时前
配置多区域OSPF
网络·智能路由器