1. 写在前面
这篇教程,我不打算只讲"代码是什么意思",我会站在我自己搭一个接口自动化项目 的角度,把这套项目从头到尾串起来。
我想做的不是"把几个 Python 文件拼起来跑通",而是把它整理成一套我以后遇到新系统时还能复用的思路。
我这套项目现在验证的是一个典型的认证链路:
- 我先调用登录接口
- 登录成功后拿到
token - 我再带着
token去获取用户信息 - 然后我调用退出登录接口
- 最后我再次访问用户信息接口,验证旧
token已经失效
这个思路,已经体现在我的配置文件、YAML 测试数据、公共工具封装、conftest.py 的 fixture 设计,以及 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_DIR、os.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 里,我定义了这几类场景:
- 正确登录
- 密码错误
- 用户名为空
- 密码为空
而且每条测试数据都被我拆成了:
titlerequestexpect
我这样写,不只是为了"看起来整齐",而是为了把一条测试用例拆成完整结构:
- 标题是什么
- 请求传什么
- 预期返回什么
示例:
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.py 和 conftest.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_database_urllogin_urluserinfo_urllogout_urltimeout
这样我在测试函数里就可以直接写:
python
def test_xxx(login_url, timeout):
而不用自己重复拼地址。
9.3 我还把 YAML 数据读取也集中到了这里
除了配置,我还在 conftest.py 里定义了读取测试数据的 fixture:
login_casesuserinfo_casesinvalid_token_caseslogout_casesinvalid_logout_cases
这样设计的好处是:
以后如果我换了 YAML 文件路径,或者增加一层数据处理逻辑,我只需要改 conftest.py,而不是每个测试文件都改。
9.4 login_token 和 fresh_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() 里,我的思路很固定:
- 从 case 里拿到请求数据
- 发登录请求
- 解析响应 JSON
- 断言
code - 断言
msg - 根据
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() 去访问用户信息接口。
接着我不仅断言:
codemsg
还断言了:
usernamenameagerole
这说明在我写这个测试时,我已经不是只在测"能不能访问成功",而是在测:
返回的业务数据是不是正确。
11.3 我在异常场景里测了什么
异常部分我测了两种情况:
- token 为空
- token 错误
这里我没有复用 login_token,而是直接从 YAML 里读错误请求头去测试。
这让我能验证系统在认证失败时,是否返回了我预期的 401 或 403。
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_token 和 fresh_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.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. 最后,我给自己的一个提醒
做完这套项目后,我会提醒自己一件事:
我以后看到一个系统,不要只想着怎么调接口,而要先想清楚:这个接口属于哪条业务链路,它依赖什么状态,它会不会影响别的测试,它的正常和异常场景分别是什么。
只要我能一直这样去想,我做的就不再是"零散测试脚本",而是在逐步形成自己的测试思维。