1.需求分析
理解业务需求,若是针对未参与的项⽬实施接⼝⾃动化,应与业务⼈员、产品经理等沟通,了解接口所⽀持的业务场景和业务逻辑。根据业务需求,明确接⼝需要实现的具体功能,如数据的获取、修改、删除等操作,以及接⼝的输⼊输出要求。分析接⼝之间的依赖关系,确定接⼝的调⽤顺序和依赖条件。
2.挑选接口
博客系统中接⼝较少,可以针对所有的接⼝实施⾃动化测试。
若是⼤型项⽬,可按照之前讲的接⼝⾃动化流程中⸺挑选接⼝内容参考挑选

3.设计测试用例
以博客系统为例
访问链接: 博客列表页
博客系统登录账号密码: zhangsan/123456, lisi/123456

该测试用例仅做参考,大家可以继续发散设计出更多更好的测试用例~
4.设计自动化测试项目的架构
语⾔选择:python
技术栈:pytest框架、requests模块、PyYAML模块、jsonschema模块、allure-pytest模块·logging
模块
集成开发环境:pycharm
blog_ApiAutoTest/
├── .venv/ # Python虚拟环境目录
├── allure-reports/ # 打包导出的完整Allure网页报告
│ ├── export/
│ ├── history/
│ ├── plugin/
│ ├── widgets/
│ ├── app.js
│ ├── favicon.ico
│ ├── index.html # 报告首页,直接打开即可查看测试结果
│ └── styles.css
├── allure-results/ # 测试结果输出⽬录
│ └── *.json
├── cases/ # 全部测试用例脚本目录,按接口拆分文件
│ ├── test_add.py # 新增博客接口用例
│ ├── test_detail.py # 博客详情接口用例
│ ├── test_getAuthorInfo.py # 获取作者信息接口用例
│ ├── test_getUserInfo.py # 获取登录用户信息接口用例
│ ├── test_list.py # 博客列表接口用例
│ └── test_login.py # 登录接口用例
├── data/ # 测试数据存放目录
│ └── data.yml # 全局公共测试数据、账号、域名、参数等
├── logs/ # 分层日志文件,按日期区分
│ ├── 2026-06-08.log
│ ├── 2026-06-08-err.log # 错误级别日志
│ ├── 2026-06-08-info.log # 普通信息日志
│ ├── 2026-06-09.log
│ ├── 2026-06-09-err.log
├── utils/ # 通用工具封装目录
│ ├── logger_util.py # 日志工具封装,统一日志输出格式
│ ├── request_util.py # 接口请求封装,统一处理header、token、请求方法
│ └── yaml_util.py # yaml文件读写工具,读取data目录测试数据
└── pytest.ini # pytest全局运行配置文件,用例过滤、日志、allure基础配置
5.编写代码
pytest==8.3.2
allure-pytest==2.13.5
jsonschema==4.23.0
PyYAML==6.0.1
requests==2.31.0
环境准备:统一配置 requirements.txt 依赖文件,一键安装所有自动化所需第三方库,完成项目运行环境搭建。
pip install -r requirements.txt
5.1 封装工具类/方法
#utils/request.util.py
import requests
from utils.logger_util import logger
host = "http://49.235.61.184:19090/"
class Request:
#创建一个能打日志的工具,赋值给变量 log
log = logger.getlog()
def get(self,url,**kwargs):
self.log.info("准备发起get请求,url:"+url)
self.log.info("接口信息:{}".format(kwargs))
r = requests.get(url = url,**kwargs)
self.log.info("接口响应状态码:{}".format(r.status_code))
self.log.info("接口响应内容:{}".format(r.text))
return r
def post(self,url,**kwargs):
self.log.info("准备发起post请求,url:"+url)
self.log.info("接口信息:{}".format(kwargs))
r = requests.post(url = url,**kwargs)
self.log.info("接口响应状态码:{}".format(r.status_code))
self.log.info("接口响应内容:{}".format(r.text))
return r
封装日志
#utils/logger_util.py
import logging
import os.path
import time
#继承关系
class infoFilter(logging.Filter):
def filter(self,record):
return record.levelno == logging.INFO
class errFilter(logging.Filter):
def filter(self,record):
return record.levelno == logging.ERROR
# 定义一个日志工具类(名字叫 logger)
class logger:
#获取日志对象—定义类方法@classmethod
# 类方法:不用创建对象,直接用 类名.getlog() 就能调用
@classmethod
def getlog(cls):
# 创建一个【日志对象】,__name__ 是当前模块名(给日志起个名字)
cls.logger = logging.getLogger(__name__)
#设置日志级别:DEBUG 级别(所有日志都能输出)
cls.logger.setLevel(logging.DEBUG)
#保证logs文件必须创建好了
LOG_PATH = "./logs/"
#如果不存在则创建
if not os.path.exists(LOG_PATH):
os.mkdir(LOG_PATH)
# 将日志输出到日志文件中
'''
logs
2026-6-8.log
2026-6-8-info.log
2026-6-8-err.log
'''
now = time.strftime("%Y-%m-%d")
log_name = LOG_PATH +now + ".log"
info_log_name = LOG_PATH + now + "-info.log"
err_log_name = LOG_PATH + now + "-err.log"
# 创建文件处理器
#FileHandler:文件处理器 → 日志写进文件
all_handler = logging.FileHandler(log_name,encoding = "utf-8")
info_handler = logging.FileHandler(info_log_name,encoding = "utf-8")
err_handler = logging.FileHandler(err_log_name,encoding = "utf-8")
#创建处理器,将日志输出到控制台
streamHandler = logging.StreamHandler()
streamHandler.setLevel(logging.INFO)
#设置日志的格式
formatter = logging.Formatter(
"%(asctime)s %(levelname)s [%(name)s] [%(filename)s (%(funcName)s:%(lineno)d)] – %(message)s"
)
#setFormatter设置格式
all_handler.setFormatter(formatter)
info_handler.setFormatter(formatter)
err_handler.setFormatter(formatter)
streamHandler.setFormatter(formatter)
#添加过滤器
#作用:只让 info 日志进入 info_handler 文件
info_handler.addFilter(infoFilter())
err_handler.addFilter(errFilter())
# 把处理器绑定到Logger
cls.logger.addHandler(all_handler)
cls.logger.addHandler(info_handler)
cls.logger.addHandler(err_handler)
#输出到控制台
# cls.logger.addHandler(streamHandler)
return cls.logger
封装读取 yml
#utils/yaml_util.py
'''
yaml相关的操作
'''
import os
import yaml
#往yaml文件中写入数据
#os.getcwd()获取当前 Python 程序正在运行的文件夹路径
def write_yaml(filename,data):
with open(os.getcwd()+"/data/"+filename,mode="a+",encoding="utf-8") as f:
yaml.safe_dump(data, stream=f)
#读取yaml文件中的数据
def read_yaml(filename,key):
with open(os.getcwd()+"/data/"+filename,mode="r",encoding="utf-8") as f:
data = yaml.safe_load(f)
return data[key]
#清空
def clear_yaml(filename):
with open(os.getcwd()+"/data/"+filename,mode="r",encoding="utf-8") as f:
f.truncate()
test_login.py(登录接口)
'''
登录——接口自动化测试
url:http://49.235.61.184:19090/user/login POST
form-data {"username":"zhangsan","password":"123456"}
'''
import re
import pytest
from jsonschema import validate
from utils.request_util import host, Request
from utils.yaml_util import write_yaml
@pytest.mark.order(1)
class TestLogin:
url = host + "user/login"
print(url)
schema = {
"type": "object",
"required": ["code", "errMsg", "data"],
"additionalProperties":False,
"properties": {
"code": {
"type": "string",
},
"errMsg": {
"type": "string"
},
"data": {
"type": ["string",'null']
}
}
}
@pytest.mark.parametrize("login",[
# 错误的账号和密码
{
"username":"zhang",
"password":"123",
"errMsg":"用户不存在"
},
# 错误的账号,正确的密码
{
"username": "zhang",
"password": "123456",
"errMsg": "用户不存在"
},
# 正确的账号,错误的密码
{
"username": "zhangsan",
"password": "123",
"errMsg": "密码错误"
},
# 不存在的账号
{
"username": "bitetest",
"password": "xxxxxx",
"errMsg": "用户不存在"
},
# 账号和密码都为空
{
"username": "",
"password": "",
"errMsg": "账号或密码不能为空"
},
# 过长的账号
{
"username": "这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号",
"password": "123456",
"errMsg": "用户不存在"
},
# 过长的密码
{
"username": "zhangsan",
"password": "这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码",
"errMsg": "密码错误"
}
])
# 异常登录——放在正常登录之前 ?
def test_login_fail(self,login):
data={
"username":login["username"],
"password":login["password"],
}
r = Request().post(url=self.url,data=data)
validate(r.json(),self.schema)
assert r.json()["code"]== "FAIL"
assert r.json()['errMsg'] == login["errMsg"]
#正常登录
@pytest.mark.parametrize("login",[
{
"username": "zhangsan",
"password": "123456",
},
{
"username": "lisi",
"password": "123456",
}
])
def test_login_success(self, login):
data = {
"username":login["username"],
"password":login["password"],
}
r = Request().post(url=self.url,data=data)
validate(r.json(),self.schema)
assert r.json()["code"]== "SUCCESS"
assert re.match('\\S{100,}', r.json()['data'])
# 接口返回的data就是用户的登录凭证—-作为其他接口的登录凭证
taken = {
"user_token_header":r.json()['data']
}
write_yaml("data.yml",taken)
test_list.py 获取博客列表
import pytest
from jsonschema import validate
from utils.request_util import host, Request
from utils.yaml_util import read_yaml, write_yaml
@pytest.mark.order(2)
class TestList:
url = host + "blog/getList"
schema = {
"type": "object",
"required": ["code", "errMsg","data"],
"additionalProperties": False,
"properties": {
"code": {
"type": "string"
},
"errMsg": {
"type": "string"
},
"data": {
"type": "array",
# 要求返回的博客数据最少要有一个
"minItems": 1,
"items": {
"type": "object",
"required": ["id","title","content","userId","deleteFlag","createTime","updateTime","loginUser"],
"additionalProperties": False,
"properties": {
"id": {
"type": "number"
},
"title": {
"type": "string"
},
"content": {
"type": "string"
},
"userId": {
"type": "number"
},
"deleteFlag": {
"type": "number"
},
"createTime": {
"type": "string"
},
"updateTime": {
"type": "string"
},
"loginUser": {
"type": "boolean"
}
}
}
}
}
}
# 未登录的状态下请求列表页–401
def test_list_noLogin(self):
r = Request().get(url=self.url)
assert r.status_code == 401
#请求列表页——登录的场景下
def test_list_login(self):
token = read_yaml("data.yml","user_token_header")
header = {
"user_token_header":token
}
r = Request().get(url=self.url,headers=header)
# jsonSchema校验
validate(r.json(),self.schema)
# 关键字段值的校验
assert r.json()["code"] == "SUCCESS"
# 提取有效的blogId存储在yaml文件中
blogId = {
"blogId":r.json()['data'][0]["id"]
}
write_yaml("data.yml",blogId)
test_add.py 新增博客
import pytest
from jsonschema import validate
from utils.request_util import host, Request
from utils.yaml_util import read_yaml
class TestAdd:
url = host +"blog/add"
schema = {
"type": "object",
"required": ["code", "errMsg", "data"],
"additionalProperties": False,
"properties": {
"code": {
"type": "string"
},
"errMsg": {
"type": "string"
},
"data": {
"type": "boolean"
}
}
}
# 未登录状态下请求add接口
def test_add_noLogin(self):
r = Request().post(url=self.url)
assert r.status_code == 401
# 测试添加博客——添加成功+添加失败
@pytest.mark.parametrize("add",[
# 添加成功
{
"title":"接口自动化标题",
"content":"接口自动化内容",
"data":True
},
# 标题为空
{
"title": "",
"content": "接口自动化内容",
"data": False
},
# 内容为空
{
"title": "接口自动化标题",
"content": "",
"data": False
},
# 标题和内容都为空
{
"title": "",
"content": "",
"data": False
},
# 添加带有图片的博客
{
"title": "接口自动化标题–带有图片",
"content": "",
"data": True
},
# 添加带有链接的博客
{
"title": "接口自动化标题–带有链接",
"content": "[百度首页](http://www.baidu.com \\"百度首页\\")",
"data": True
}
])
def test_add(self,add):
token = read_yaml("data.yml","user_token_header")
header = {
"user_token_header":token
}
json = {
"title":add["title"],
"content":add["content"]
}
r = Request().post(url=self.url,json = json,headers=header)
# 验证jsonSchema
validate(r.json(),self.schema)
# assert关键字段的值要匹配
assert r.json()['data'] == add["data"]
test_detail.py 获取博客详情
import token
import pytest
from utils.request_util import host, Request
from utils.yaml_util import read_yaml
from jsonschema import validate
class TestDetail:
url = host + "blog/getBlogDetail"
schema = {
"type": "object",
"required": ["code", "errMsg","data"],
"additionalProperties": False,
"properties": {
"code": {
"type": "string"
},
"errMsg": {
"type": "string"
},
"data": {
"type": "object",
"required": ["id","title","content","userId","deleteFlag","createTime","updateTime","loginUser"],
"additionalProperties": False,
"properties": {
"id": {
"type": "number"
},
"title": {
"type": "string"
},
"content": {
"type": "string"
},
"userId": {
"type": "number"
},
"deleteFlag": {
"type": "number"
},
"createTime": {
"type": "string"
},
"updateTime": {
"type": "string"
},
"loginUser": {
"type": "boolean"
}
}
}
}
}
# 未登录状态下访问博客详情页
def test_detail_noLogin(self):
url = self.url + "?blogId=123"
r = Request().get(url=url)
assert r.status_code == 401
# 登录状态下请求博客详情页
def test_detail_login(self):
url = self.url + "?blogId=" + str(read_yaml("data.yml","blogId"))
token = read_yaml("data.yml","user_token_header")
header = {
"user_token_header": token
}
r = Request().get(url=url,headers=header)
# jsonSchema校验
validate(instance=r.json(), schema=self.schema)
# assert关键字段值的校验
assert r.json()["code"] == "SUCCESS"
# 博客详情页——blogId错误
@pytest.mark.parametrize("blogId", ["", 1234, "你好", -100, 999999999999999999999999999999999999])
def test_detail_fail(self,blogId):
url = self.url
#配置参数
params = {"blogId": blogId}
token = read_yaml("data.yml","user_token_header")
header = {
"user_token_header": token
}
r = Request().get(url=url,headers=header)
expect_json = {
"code": "FAIL",
"errMsg": "内部错误, 请联系管理员",
"data": None
}
assert r.json() == expect_json
test_getAuthorInfo.py 根据博客 id 查作者
from unittest import expectedFailure
import pytest
from jsonschema import validate
from utils.request_util import host, Request
from utils.yaml_util import read_yaml
class TestgetAuthorInfo:
url = host + "user/getAuthorInfo"
schema = {
"type": "object",
"required": ["code","errMsg","data"],
"additionalProperties": False,
"properties": {
"code": {
"type": "string"
},
"errMsg": {
"type": "string"
},
"data": {
"type": ["object",'null'],
"additionalProperties": False,
"required": ["id","userName","password","githubUrl","deleteFlag","createTime","updateTime"],
"properties": {
"id": {
"type": "number"
},
"userName": {
"type": "string"
},
"password": {
"type": "string"
},
"githubUrl": {
"type": "string"
},
"deleteFlag": {
"type": "number"
},
"createTime": {
"type": "string"
},
"updateTime": {
"type": "string"
}
}
}
}
}
# 未登录状态访问接口
def test_getAuthorInfo_noLogin(self):
url = self.url + "?blogId=2234"
r = Request().get(url=url)
assert r.status_code == 401
# 登录状态下正确请求
# 有效的blogId
def test_getAuthorInfo(self):
blogId = read_yaml("data.yml","blogId")
url = self.url + "?blogId=" + str(blogId)
# 读取用户登录凭证
token = read_yaml("data.yml","user_token_header")
header = {
"user_token_header": token
}
# 发起请求
r = Request().get(url=url, headers=header)
# 校验jsonschema
validate(r.json(),self.schema)
# assert校验关键数据
assert r.json()["code"] == "SUCCESS"
# 登录状态下异常请求
@pytest.mark.parametrize("blogId,expected_code",[
("","FAIL"),
(1811,"FAIL"),
("你好","FAIL"),
(-100,"SUCCESS"),
(999999999999999999999999999999999999,"FAIL")
])
def test_getAuthorInfo_fail(self,blogId,expected_code):
url = self.url
# 配置参数
params = {"blogId":blogId}
# 读取用户登录凭证
token = read_yaml("data.yml","user_token_header")
header = {
"user_token_header": token
}
# 发起请求
r = Request().get(url=url, headers=header,params=params)
validate(r.json(),self.schema)
assert r.json()["code"] == expected_code
test_getUserInfo.py 获取当前登录用户信息
from utils.request_util import host, Request
from utils.yaml_util import read_yaml
from jsonschema import validate
class TestGetUserInfo:
url = host + "user/getUserInfo"
schema = {
"type": "object",
"required": ["code","errMsg","data"],
"additionalProperties": False,
"properties": {
"code": {
"type": "string"
},
"errMsg": {
"type": "string"
},
"data": {
"type": "object",
"additionalProperties": False,
"required": ["id","userName","password","githubUrl","deleteFlag","createTime","updateTime"],
"properties": {
"id": {
"type": "number"
},
"userName": {
"type": "string"
},
"password": {
"type": "string"
},
"githubUrl": {
"type": "string"
},
"deleteFlag": {
"type": "number"
},
"createTime": {
"type": "string"
},
"updateTime": {
"type": "string"
}
}
}
}
}
# 未登陆状态下请求接口
def test_getUserInfo_noLogin(self):
r = Request().get(url=self.url)
assert r.status_code == 401
# 登陆状态下请求接口
def test_getUserInfo(self):
# 添加请求头
token = read_yaml("data.yml","user_token_header")
header = {
"user_token_header": token
}
r = Request().get(url=self.url,headers=header)
# 校验jsonschema
validate(r.json(),self.schema)
assert r.json()["code"] == "SUCCESS"
补充:指定⽤户执⾏顺序
在使⽤pytest进⾏测试时,有时候我们需要按照特定的顺序来运⾏测试⽤例,尤其是在涉及到测试⽤例之间的依赖关系时。pytest本⾝并不直接⽀持通过配置来改变测试⽤例的默认运⾏顺序,pytest-order是⼀个第三⽅插件,专⻔⽤于控制测试⽤例的执⾏顺序。⾸先,你需要安装这个插件:
pip install pytest-order==1.3.0
既可以用在测试类上,也可以用在测试⽅法上,以测试类为例:
@pytest.mark.order(1)
def test_one():
assert True
@pytest.mark.order(2)
def test_two():
assert True
执⾏结果:

6 执行测试用例

7.生成测试报告并分析结果
allure serve allure-results
allure generate .\\allure-results\\ -o .\\allure-reports –clean






