文章目录
- 博客系统接口自动化测试报告
-
- [1. 测试概述](#1. 测试概述)
-
- [1.1 测试背景](#1.1 测试背景)
- [1.2 测试目标](#1.2 测试目标)
- [1.3 测试范围](#1.3 测试范围)
- [2. 测试大纲](#2. 测试大纲)
- [3. 测试环境](#3. 测试环境)
- [4. 用例设计与执行](#4. 用例设计与执行)
-
- [4.1 用例分类](#4.1 用例分类)
- [4.2 测试数据管理](#4.2 测试数据管理)
- [4.3 关键代码结构](#4.3 关键代码结构)
-
- [1. HTTP 请求封装 (`utils/request_util.py`)](#1. HTTP 请求封装 (
utils/request_util.py))
- [2. YAML 读写工具 (`utils/yaml_util.py`)](#2. YAML 读写工具 (
utils/yaml_util.py))
- [3. 日志工具 (`utils/logger_util.py`)](#3. 日志工具 (
utils/logger_util.py))
- [4. pytest 配置文件 (`pytest.ini`)](#4. pytest 配置文件 (
pytest.ini))
- [5. 登录接口测试用例 (`cases/test_login.py`)](#5. 登录接口测试用例 (
cases/test_login.py))
- [6. 博客列表接口测试用例 (`cases/test_list.py`)](#6. 博客列表接口测试用例 (
cases/test_list.py))
- [7. 博客详情接口测试用例 (`cases/test_detail.py`)](#7. 博客详情接口测试用例 (
cases/test_detail.py))
- [8. 新增博客接口测试用例 (`cases/test_add.py`)](#8. 新增博客接口测试用例 (
cases/test_add.py))
- [9. 用户信息接口测试用例 (`cases/test_getUserInfo.py`)](#9. 用户信息接口测试用例 (
cases/test_getUserInfo.py))
- [10. 作者信息接口测试用例 (`cases/test_getAuthorInfo.py`)](#10. 作者信息接口测试用例 (
cases/test_getAuthorInfo.py))
- [11. 项目依赖清单 (`requirements.txt`)](#11. 项目依赖清单 (
requirements.txt))
- [5. 测试执行结果](#5. 测试执行结果)
-
- [5.1 总体统计](#5.1 总体统计)
- [5.2 详细结果](#5.2 详细结果)
-
- [5.2.1 登录接口 (`/user/login`)](#5.2.1 登录接口 (
/user/login))
- [5.2.2 博客列表接口 (`/blog/getList`)](#5.2.2 博客列表接口 (
/blog/getList))
- [5.2.3 发布博客接口 (`/blog/add`)](#5.2.3 发布博客接口 (
/blog/add))
- [5.2.4 博客详情接口 (`/blog/getBlogDetail`)](#5.2.4 博客详情接口 (
/blog/getBlogDetail))
- [5.2.5 用户信息与作者信息接口](#5.2.5 用户信息与作者信息接口)
- [6. 缺陷分析](#6. 缺陷分析)
-
- [6.1 发现的缺陷](#6.1 发现的缺陷)
- [6.2 改进建议](#6.2 改进建议)
- [7. 测试结论](#7. 测试结论)
- [8. 附件](#8. 附件)
-
- [8.1 测试执行日志](#8.1 测试执行日志)
- [8.2 自动化项目代码仓库](#8.2 自动化项目代码仓库)
博客系统接口自动化测试报告
1. 测试概述
1.1 测试背景
本次测试针对博客系统核心业务接口进行自动化验证,确保系统各接口在功能、异常处理、安全认证及数据交互方面符合设计要求。
1.2 测试目标
- 验证登录、博客列表、博客详情、博客发布、用户信息、作者信息等接口的正确性
- 覆盖正常场景、异常场景(参数缺失、类型错误、边界值、未授权访问)
- 确保接口返回数据结构符合预期 JSON Schema
- 测试请求方法约束(GET / POST)
1.3 测试范围
| 模块 |
接口 |
方法 |
是否认证 |
| 登录 |
/user/login |
POST |
否 |
| 博客列表 |
/blog/getList |
GET |
是 |
| 博客详情 |
/blog/getBlogDetail |
GET |
是 |
| 发布博客 |
/blog/add |
POST |
是 |
| 用户信息 |
/user/getUserInfo |
GET |
是 |
| 作者信息 |
/user/getAuthorInfo |
GET |
是 |
2. 测试大纲
3. 测试环境
| 项目 |
内容 |
| 服务地址 |
http://49.235.61.184:19090/ |
| 测试账号 |
zhangsan / 123456 lisi / 123456 |
| 自动化框架 |
Python 3.9 + pytest 8.3.2 + requests 2.31.0 |
| 辅助库 |
PyYAML, jsonschema, allure-pytest, pytest-order |
| 报告工具 |
Allure Report 2.30.0 |
| 运行平台 |
Windows 10 / macOS |
4. 用例设计与执行
4.1 用例分类
- 功能验证:正常登录、获取列表、发布博客等
- 参数校验:空值、缺失、类型错误、超长字符串
- 安全验证:无 token 访问、错误 token、请求方法非法
- 数据依赖:token 传递、博客 ID 链式传递
4.2 测试数据管理
使用 YAML 文件(data.yml)存储共享数据:
user_token_header:登录成功后的 token
blogId:从列表接口获取的第一个博客 ID
4.3 关键代码结构
Blog_ApiAutoTest/
├── .venv/
├── allure-reports/
├── allure-results/
├── cases/
│ ├── test_add.py
│ ├── test_detail.py
│ ├── test_getAuthorInfo.py
│ ├── test_getUserInfo.py
│ ├── test_list.py
│ └── test_login.py
├── data/
│ └── data.yml
├── logs/
├── utils/
│ ├── logger_util.py
│ ├── request_util.py
│ └── yaml_util.py
├── pytest.ini
├── requirements.txt
└── run.py
1. HTTP 请求封装 (utils/request_util.py)
import requests
# 导入自定义的日志工具模块,用于记录请求和响应信息
from utils.logger_util import logger
# 定义接口基础域名(测试环境地址)
host = "http://49.235.61.184:19090/"
class Request:
"""
封装 HTTP 请求类,统一处理 GET 和 POST 请求,
并在请求前后自动记录日志(请求参数、响应状态码、响应内容)
"""
# 从日志模块获取日志记录器实例(注意:原代码使用的是 logger.getlog() 方法)
log = logger.getlog()
def get(self, url, **kwargs):
"""
发送 HTTP GET 请求
:param url: 请求地址(注意:此 url 需要包含完整路径,可与 host 拼接后传入)
:param kwargs: 其他 requests.get 支持的参数,如 params, headers, cookies 等
:return: requests.Response 对象
"""
# 记录开始发起 GET 请求的日志
self.log.info('准备开始发起get请求,url:' + url)
# 记录接口的其他参数信息(如 headers、params 等)
self.log.info("接口信息:{}".format(kwargs))
# 调用 requests 库发送 GET 请求
r = requests.get(url=url, **kwargs)
# 记录响应状态码
self.log.info("接⼝响应状态码:{}".format(r.status_code))
# 记录响应正文(文本格式)
self.log.info("接⼝响应信息:{}".format(r.text))
# 返回 Response 对象,供测试用例进一步断言
return r
def post(self, url, **kwargs):
"""
发送 HTTP POST 请求
:param url: 请求地址
:param kwargs: 其他 requests.post 支持的参数,如 data, json, headers 等
:return: requests.Response 对象
"""
# 记录开始发起 POST 请求的日志
self.log.info('准备开始发起post请求,url:' + url)
# 记录接口的其他参数信息
self.log.info("接口信息:{}".format(kwargs))
# 调用 requests 库发送 POST 请求
r = requests.post(url=url, **kwargs)
# 记录响应状态码
self.log.info("接⼝响应状态码:{}".format(r.status_code))
# 记录响应正文
self.log.info("接⼝响应信息:{}".format(r.text))
return r
2. YAML 读写工具 (utils/yaml_util.py)
import os
import yaml
# 该模块提供对 YAML 配置文件的写入、读取和清空功能
# 所有文件操作均基于当前工作目录下的 data/ 文件夹
def write_yaml(filename, data):
"""
将数据以 YAML 格式追加写入到指定文件中
:param filename: 文件名(例如 "data.yml"),文件将存放在当前目录的 data/ 子目录下
:param data: 要写入的 Python 数据结构(字典、列表等),会被 safe_dump 转换为 YAML 格式
:return: None
"""
# os.getcwd() 获取当前工作目录,拼接 "/data/" 和文件名得到完整路径
# 以追加模式 'a+' 打开文件,如果文件不存在则创建,写入位置在文件末尾
with open(os.getcwd() + "/data/" + filename, 'a+', encoding='utf-8') as f:
# yaml.safe_dump 将 data 安全地序列化为 YAML 格式并写入文件
# safe_dump 不会执行任意代码,比 dump 更安全
yaml.safe_dump(data, f)
def read_yaml(filename, key):
"""
从 YAML 文件中读取数据,并返回指定键对应的值
:param filename: 文件名(位于 data/ 目录下)
:param key: 要读取的顶层键名(字符串)
:return: 该键对应的值(类型取决于 YAML 内容)
:raises: 如果文件不存在或键不存在会抛出异常,调用方需自行处理
"""
# 以只读模式 'r' 打开 YAML 文件
with open(os.getcwd() + "/data/" + filename, 'r', encoding='utf-8') as f:
# yaml.safe_load 安全地解析 YAML 文件内容,返回 Python 对象(通常是字典)
data = yaml.safe_load(f)
# 根据键返回对应的值
return data[key]
def clear_yaml(filename):
"""
清空指定 YAML 文件的内容(将文件截断为 0 字节)
:filename: 文件名(位于 data/ 目录下)
:return: None
"""
# 以写入模式 'w' 打开文件,该模式会清空原有内容
with open(os.getcwd() + "/data/" + filename, 'w', encoding='utf-8') as f:
# f.truncate() 截断文件,由于已经以 'w' 模式打开,文件已被清空,此步可省略但保留以示明确
f.truncate()
3. 日志工具 (utils/logger_util.py)
# 导入Python内置的日志模块,用于记录日志
import logging
# 导入操作系统模块,用于创建文件夹、判断路径是否存在
import os
import sys
# 导入时间模块,用于生成日志文件名中的日期
import time
# 定义INFO级别的过滤器,只允许INFO级别日志通过
class infoFilter(logging.Filter):
def filter(self, record):
# 判断日志级别是否等于 INFO,是则返回True(放行)
return record.levelno == logging.INFO
# 定义ERROR级别的过滤器,只允许ERROR级别日志通过
class errorFilter(logging.Filter):
def filter(self, record):
# 判断日志级别是否等于 ERROR,是则返回True(放行)
return record.levelno == logging.ERROR
# 日志工具类,封装日志功能
class logger:
# 类方法,获取配置好的日志对象
@classmethod
def getlog(cls):
# 创建一个logger日志器对象,__name__是当前模块名
cls.logger = logging.getLogger(__name__)
# 设置日志器最低输出级别为DEBUG(所有级别都能输出)
cls.logger.setLevel(logging.DEBUG)
# 定义日志存放的文件夹路径
LOG_PATH = "./logs/"
# 如果日志文件夹不存在,则创建
if not os.path.exists(LOG_PATH):
os.makedirs(LOG_PATH)
# 获取当前日期,格式:年-月-日,用于日志文件名
now = time.strftime("%Y-%m-%d")
# 定义三种日志文件路径
all_log = LOG_PATH + now + ".log" # 全部日志
info_log = LOG_PATH + now + "-info.log" # 只存INFO日志
error_log = LOG_PATH + now + "-error.log" # 只存ERROR日志
# 创建三个文件处理器,负责把日志写入对应文件
handler = logging.FileHandler(all_log, encoding="utf-8")
info_handler = logging.FileHandler(info_log, encoding="utf-8")
error_handler = logging.FileHandler(error_log, encoding="utf-8")
stream_handler = logging.StreamHandler()
# 给处理器绑定过滤器,实现日志分流
info_handler.addFilter(infoFilter()) # 只写入INFO
error_handler.addFilter(errorFilter()) # 只写入ERROR
# 定义日志输出格式
formatter = logging.Formatter(
"%(asctime)s " # 日志时间
"%(levelname)s " # 日志级别
"[%(name)s] " # 日志器名称
"[%(filename)s " # 当前文件名
"(%(funcName)s:" # 当前函数名
"%(lineno)d] " # 当前行号
"- %(message)s" # 日志信息
)
# 给三个处理器设置统一的日志格式
handler.setFormatter(formatter)
info_handler.setFormatter(formatter)
error_handler.setFormatter(formatter)
stream_handler.setFormatter(formatter)
# 把三个处理器都添加到日志器中
cls.logger.addHandler(handler)
cls.logger.addHandler(info_handler)
cls.logger.addHandler(error_handler)
# cls.logger.addHandler(stream_handler)
# 返回配置完成的logger对象,供外部使用
return cls.logger
4. pytest 配置文件 (pytest.ini)
[pytest]
addopts = -vs --alluredir=allure-results
5. 登录接口测试用例 (cases/test_login.py)
import re
import pytest
# 导入自定义工具:host(服务器地址)、Request(封装请求)
from utils.request_util import host, Request
# 导入 JSON Schema 校验器
from jsonschema import validate
# 导入写入 YAML 的工具,用于保存登录成功后的 token
from utils.yaml_util import write_yaml
# 使用 pytest-order 插件,标记当前测试类执行顺序为第一位(登录接口优先执行,其他接口依赖 token)
@pytest.mark.order(1)
class TestLogin:
# 登录接口的完整 URL
url = host + "user/login"
# JSON Schema:用于校验接口返回的 JSON 结构是否符合预期
schema = {
"type": "object", # 返回结果应是一个对象
"required": ["code", "errMsg", "data"], # 必须包含这三个字段
"additionalProperties": False, # 不允许出现未定义的字段
"properties": {
"code": {"type": "string"}, # code 字段必须是字符串
"errMsg": {"type": "string"}, # errMsg 字段必须是字符串
"data": {"type": ["string", "null"]} # data 字段可以是字符串或 null
}
}
# 登录失败场景的参数化测试,每个元组包含用户名、密码及期望的错误信息
@pytest.mark.parametrize("login", [
{
"username": "zhang",
"password": "123",
"errMsg": "用户不存在"
},
{
"username": "zhang",
"password": "123456",
"errMsg": "用户不存在"
},
{
"username": "zhangsan",
"password": "123",
"errMsg": "密码错误"
},
{
"username": "lili",
"password": "123",
"errMsg": "用户不存在"
},
{
"username": "",
"password": "",
"errMsg": "账号或密码不能为空"
},
{
"username": "zhangsan",
"password": "",
"errMsg": "账号或密码不能为空"
},
{
"username": "",
"password": "123456",
"errMsg": "账号或密码不能为空"
},
{
"username": "这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号这是一个很长的账号",
"password": "123456",
"errMsg": "用户不存在"
},
{
"username": "zhangsan",
"password": "这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码这是一个很长的密码",
"errMsg": "密码错误"
},
])
def test_login_fail(self, login):
"""
登录失败测试用例
验证:错误的用户名/密码/空值/超长值等,服务器返回 FAIL 状态码及对应的错误提示
"""
# 构造请求体(表单格式)
data = {
"username": login["username"],
"password": login["password"]
}
# 实例化请求工具类,发送 POST 请求
req = Request()
r = req.post(url=self.url, data=data)
# 1. 用 JSON Schema 校验响应结构
validate(instance=r.json(), schema=self.schema)
# 2. 断言业务状态码为失败
assert r.json()["code"] == "FAIL"
# 3. 断言错误信息与预期一致
assert r.json()["errMsg"] == login["errMsg"]
# 登录成功场景的参数化测试,支持多个有效账号
@pytest.mark.parametrize("login", [
{"username": "zhangsan", "password": "123456"},
{"username": "lisi", "password": "123456"}
])
def test_login_success(self, login):
"""
登录成功测试用例
验证:正确的用户名密码应返回 SUCCESS,并生成 token,同时将 token 写入 YAML 文件供后续接口使用
"""
data = {
"username": login["username"],
"password": login["password"]
}
req = Request()
r = req.post(url=self.url, data=data)
# 校验响应结构
validate(instance=r.json(), schema=self.schema)
# 断言业务成功
assert r.json()["code"] == "SUCCESS"
# 断言 token 为长度至少 100 的非空白字符(正则匹配)
assert re.match("\S{100,}", r.json()["data"])
# 准备写入 YAML 的 token 数据,键为 user_token_header(供其他接口放入请求头)
token = {
"user_token_header": r.json()["data"]
}
# 将 token 写入 data 目录下的 data.yml 文件
write_yaml("data.yml", token)
6. 博客列表接口测试用例 (cases/test_list.py)
import pytest
# 导入自定义工具:host(服务器地址)、Request(封装请求)
from utils.request_util import host, Request
# 导入 YAML 读写工具:read_yaml 读取 token,write_yaml 保存数据
from utils.yaml_util import read_yaml, write_yaml
# 使用 pytest-order 插件,标记当前测试类执行顺序为第二位(登录接口之后执行,依赖登录生成的 token)
@pytest.mark.order(2)
class TestList:
# 博客列表查询接口的完整 URL
url = host + "blog/getList"
# JSON Schema:用于校验成功登录后返回的博客列表数据结构
schema = {
"type": "object", # 返回结果是一个对象
"required": ["code", "errMsg", "data"], # 必须包含 code、errMsg、data 三个字段
"additionalProperties": False, # 不允许额外字段
"properties": {
"code": {"type": "string"}, # code 字段必须是字符串
"errMsg": {"type": "string"}, # errMsg 字段必须是字符串
"data": {
"type": "array", # data 字段是一个数组,存放博客列表
"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"}
}
}
}
}
}
def test_list_noLogin(self):
"""
测试未登录状态下访问博客列表接口
预期:未携带 token 时,接口应返回 401 状态码(未授权)
"""
# 发送 GET 请求,不携带任何认证信息
r = Request().get(self.url)
# 断言响应状态码为 401(无权限)
assert r.status_code == 401
def test_list_login(self):
"""
测试已登录状态下访问博客列表接口
流程:
1. 从之前登录保存的 data.yml 文件中读取 token
2. 在请求头中携带 token 发送 GET 请求
3. 验证返回的 code 为 SUCCESS
4. 取出列表第一个博客的 id,写入 data.yml 供后续接口使用(如获取博客详情、删除博客等)
"""
# 从 YAML 文件中读取登录成功后保存的 token(键为 user_token_header)
token = read_yaml("data.yml", "user_token_header")
# 构造请求头,将 token 放入 user_token_header 字段
header = {
"user_token_header": token
}
# 发送带认证头的 GET 请求
r = Request().get(self.url, headers=header)
# 断言业务响应码为 SUCCESS
assert r.json()["code"] == "SUCCESS"
# 提取返回列表中第一个博客的 id,存入字典
blogId = {
"blogId": r.json()["data"][0]["id"]
}
# 将 blogId 追加写入 data.yml,供其他测试用例使用
write_yaml("data.yml", blogId)
7. 博客详情接口测试用例 (cases/test_detail.py)
import pytest
# 导入自定义工具:host(服务器地址)、Request(封装请求)
from utils.request_util import host, Request
# 导入 YAML 读取工具,用于获取登录 token 和 blogId
from utils.yaml_util import read_yaml
# 导入 JSON Schema 校验器
from jsonschema import validate
class TestDetail:
"""
获取博客详情接口的测试类
接口地址:/blog/getBlogDetail
请求方法:GET
请求参数:blogId(查询参数)
需要认证:是(需在请求头中携带 user_token_header)
"""
# 接口完整 URL
url = host + "blog/getBlogDetail"
# JSON Schema:用于校验成功获取博客详情时返回的数据结构
schema = {
"type": "object", # 返回结果是一个对象
"required": ["code", "errMsg", "data"], # 必须包含这三个字段
"additionalProperties": False, # 不允许额外字段
"properties": {
"code": {"type": "string"}, # code 字段必须是字符串
"errMsg": {"type": "string"}, # errMsg 字段必须是字符串
"data": {
"type": "object", # data 是一个对象,存放博客详细信息
"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):
"""
未登录状态下获取博客详情
预期:不携带 token 时,接口应返回 401 状态码(未授权)
"""
# 构造带查询参数的 URL(blogId=1234)
url = self.url + "?blogId=1234"
# 发送 GET 请求,不携带认证信息
r = Request().get(url=url)
# 断言响应状态码为 401
assert r.status_code == 401
def test_detail_login(self):
"""
已登录状态下获取博客详情(正常场景)
流程:
1. 从 YAML 文件中读取之前保存的 blogId(由列表接口写入)
2. 从 YAML 文件中读取 token
3. 携带 token 和 blogId 参数发送 GET 请求
4. 用 JSON Schema 校验响应结构
5. 断言业务响应码为 SUCCESS
"""
# 拼接 URL:直接读取 blogId 并拼接到 URL 后面
url = self.url + "?blogId=" + str(read_yaml("data.yml", "blogId"))
# 读取登录 token
user_token_header = (read_yaml("data.yml", "user_token_header"))
# 构造认证头
header = {
"user_token_header": user_token_header
}
# 发送带认证头的 GET 请求
r = Request().get(url=url, headers=header)
# 使用 JSON Schema 校验返回结构
validate(instance=r.json(), schema=self.schema)
# 断言业务成功
assert r.json()["code"] == "SUCCESS"
# 参数化测试:使用各种异常/非法的 blogId 值
@pytest.mark.parametrize("blogId", [
"", # 空字符串
1234, # 不存在的博客 ID(数字)
"lili", # 非数字字符串
-100, # 负数
666666666666666666666666666666666666666666666666666666666666 # 超长数字(超出整数范围)
])
def test_detail_fail(self, blogId):
"""
已登录状态下,使用异常/非法的 blogId 参数获取博客详情
验证接口在异常输入时应返回固定的失败响应
"""
# 读取登录 token
user_token_header = (read_yaml("data.yml", "user_token_header"))
# 构造认证头
header = {
"user_token_header": user_token_header
}
# 使用 params 参数传递 blogId(推荐方式,比直接拼接更安全)
params = {
"blogId": blogId
}
# 发送带认证头和查询参数的 GET 请求
r = Request().get(self.url, headers=header, params=params)
# 期望的返回结果(根据实际接口行为,异常输入应返回此 JSON)
expect_json = {
"code": "FAIL",
"errMsg": "内部错误, 请联系管理员",
"data": None
}
# 断言实际返回结果完全等于期望结果
assert r.json() == expect_json
8. 新增博客接口测试用例 (cases/test_add.py)
import json
from wsgiref.validate import validator
import pytest
# 导入自定义工具:host(服务器地址)、Request(封装请求)
from utils.request_util import host, Request
# 导入 YAML 读取工具,用于获取登录 token
from utils.yaml_util import read_yaml
# 导入 JSON Schema 校验器
from jsonschema import validate
class TestAdd:
"""
添加博客接口的测试类
接口地址:/blog/add
请求方法:POST
请求体格式:JSON
需要认证:是(需在请求头中携带 user_token_header)
"""
# 接口完整 URL
url = host + "blog/add"
# JSON Schema:用于校验添加博客接口的响应结构
# 注意:原代码变量名为 Schema(大写S),但不影响功能
Schema = {
"type": "object", # 返回结果是一个对象
"required": ["code", "errMsg", "data"], # 必须包含这三个字段
"additionalProperties": False, # 不允许额外字段
"properties": {
"code": {"type": "string"}, # code 字段必须是字符串
"errMsg": {"type": "string"}, # errMsg 字段必须是字符串
"data": {"type": "boolean"} # data 字段是布尔值(成功返回 True,失败返回 False)
}
}
def test_add_noLogin(self):
"""
未登录状态下添加博客
预期:不携带 token 时,接口应返回 401 状态码(未授权)
"""
# 发送 POST 请求,不携带认证信息,也不带请求体
r = Request().post(url=self.url)
# 断言响应状态码为 401
assert r.status_code == 401
# 参数化测试:测试各种博客标题和内容的组合
@pytest.mark.parametrize("add", [
{
"title": "接口自动化博客标题",
"content": "接口自动化博客详情",
"data": True # 预期返回 data=True(添加成功)
},
{
"title": "",
"content": "接口自动化博客详情",
"data": False # 标题为空,预期失败
},
{
"title": "接口自动化博客标题",
"content": "",
"data": False # 内容为空,预期失败
},
{
"title": "",
"content": "",
"data": False # 标题和内容均为空,预期失败
},
{
"title": "接口自动化博客标题picture",
"content": "",
"data": True # 内容包含 Markdown 图片,预期成功
},
{
"title": "接口自动化博客标题链接",
"content": "[百度](http://www.baidu.com \"baidu\")",
"data": True # 内容包含 Markdown 链接,预期成功
},
])
def test_add(self, add):
"""
已登录状态下添加博客(参数化)
流程:
1. 从 YAML 文件中读取登录 token
2. 构造请求头,携带 token
3. 构造 JSON 请求体(title 和 content)
4. 发送 POST 请求
5. 用 JSON Schema 校验响应结构
6. 断言返回的 data 字段与预期结果一致(True/False)
"""
# 读取登录 token
token = read_yaml("data.yml", "user_token_header")
# 构造认证头
token_header = {
"user_token_header": token
}
# 构造请求体(JSON 格式)
data_json = {
"title": add["title"],
"content": add["content"]
}
# 发送 POST 请求,使用 json 参数传递 JSON 数据,并携带认证头
r = Request().post(url=self.url, json=data_json, headers=token_header)
# 使用 JSON Schema 校验响应结构
validate(instance=r.json(), schema=self.Schema)
# 断言返回的 data 布尔值与参数化中的预期值一致
assert r.json()["data"] == add["data"]
9. 用户信息接口测试用例 (cases/test_getUserInfo.py)
# 导入自定义工具:host(服务器地址)、Request(封装请求)
from utils.request_util import host, Request
# 导入 YAML 读取工具,用于获取登录后保存的 token
from utils.yaml_util import read_yaml
# 导入 JSON Schema 校验器
from jsonschema import validate
class TestgetUserInfo:
"""
获取用户信息接口的测试类
接口地址:/user/getUserInfo
请求方法:GET
需要认证:是(需在请求头中携带 user_token_header)
"""
# 接口完整 URL
url = host + "user/getUserInfo"
# JSON Schema:用于校验登录状态下返回的用户信息数据结构
schema = {
"type": "object", # 返回结果是一个对象
"required": ["code", "errMsg", "data"], # 必须包含 code、errMsg、data 三个字段
"additionalProperties": False, # 不允许额外字段
"properties": {
"code": {"type": "string"}, # code 字段必须是字符串
"errMsg": {"type": "string"}, # errMsg 字段必须是字符串
"data": {
"type": "object", # data 是一个对象,存放用户详细信息
"required": ["id", "userName", "password", "githubUrl",
"deleteFlag", "createTime", "updateTime"], # 必须包含这些字段
"additionalProperties": False, # 不允许额外字段
"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):
"""
未登录状态下获取用户信息
预期:未携带 token 时,接口应返回 401 状态码(未授权)
"""
# 发送 GET 请求,不携带任何认证信息
r = Request().get(self.url)
# 断言响应状态码为 401
assert r.status_code == 401
def test_getUserInfo_login(self):
"""
已登录状态下获取用户信息
流程:
1. 从之前登录保存的 data.yml 文件中读取 token
2. 在请求头中携带 token 发送 GET 请求
3. 用 JSON Schema 校验响应结构
4. 断言业务响应码为 SUCCESS
"""
# 从 YAML 文件中读取登录成功后保存的 token(键为 user_token_header)
token = read_yaml("data.yml", "user_token_header")
# 构造请求头,将 token 放入 user_token_header 字段
header = {
"user_token_header": token
}
# 发送带认证头的 GET 请求
r = Request().get(self.url, headers=header)
# 使用 JSON Schema 校验返回的 JSON 结构是否符合预期
validate(instance=r.json(), schema=self.schema)
# 断言业务响应码为 SUCCESS
assert r.json()["code"] == "SUCCESS"
10. 作者信息接口测试用例 (cases/test_getAuthorInfo.py)
import pytest
# 注意:下一行导入了一个不存在的模块,可能是笔误,但按用户要求不修改代码,仅加注释
from Tools.scripts.generate_opcode_h import header
# 导入自定义工具:host(服务器地址)、Request(封装请求)
from utils.request_util import host, Request
# 导入 YAML 读取工具,用于获取登录后保存的 token 和 blogId
from utils.yaml_util import read_yaml
# 导入 JSON Schema 校验器
from jsonschema import validate
class TestgetAuthorInfo:
"""
获取作者信息接口的测试类
接口地址:/user/getAuthorInfo
请求方法:GET
请求参数:blogId(查询参数)
需要认证:是(需在请求头中携带 user_token_header)
"""
# 接口完整 URL
url = host + "user/getAuthorInfo"
# JSON Schema:用于校验接口返回的数据结构
# 注意:data 字段可以是对象或 null(当查询不存在的作者时可能为 null)
schema = {
"type": "object", # 返回结果是一个对象
"required": ["code", "errMsg", "data"], # 必须包含这三个字段
"additionalProperties": False, # 不允许额外字段
"properties": {
"code": {"type": "string"}, # code 字段必须是字符串
"errMsg": {"type": "string"}, # errMsg 字段必须是字符串
"data": {
"type": ["object", "null"], # data 可以是对象或 null(如作者不存在时)
"required": ["id", "userName", "password", "githubUrl",
"deleteFlag", "createTime", "updateTime"], # 若 data 为对象,则必须包含这些字段
"additionalProperties": False, # 不允许额外字段
"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):
"""
未登录状态下获取作者信息
预期:不携带 token 时,接口应返回 401 状态码(未授权)
"""
# 构造带查询参数 blogId=4 的完整 URL
url = self.url + "?blogId=4"
# 发送 GET 请求,不携带认证信息
r = Request().get(url=url)
# 断言响应状态码为 401
assert r.status_code == 401
def test_getAuthorInfo_login(self):
"""
已登录状态下获取作者信息(正常场景)
流程:
1. 从 YAML 文件中读取 token
2. 从 YAML 文件中读取之前保存的 blogId(由列表接口写入)
3. 携带 token 和 blogId 参数发送 GET 请求
4. 用 JSON Schema 校验响应结构
5. 断言业务响应码为 SUCCESS
"""
# 读取登录 token
token = read_yaml("data.yml", "user_token_header")
# 构造认证头
header = {
"user_token_header": token
}
# 读取之前保存的博客 ID(由 TestList 类写入)
blogId = read_yaml("data.yml", "blogId")
# 构造查询参数(GET 请求的参数应放在 params 中)
params = {
"blogId": blogId
}
# 发送带认证头和查询参数的 GET 请求
r = Request().get(url=self.url, headers=header, params=params)
# 使用 JSON Schema 校验返回结构
validate(r.json(), self.schema)
# 断言业务成功
assert r.json()["code"] == "SUCCESS"
# 参数化测试:测试各种异常的 blogId 输入
@pytest.mark.parametrize("blogId, code", [
("", "FAIL"), # 空字符串
(1234, "FAIL"), # 不存在的博客ID(数字)
("lili", "FAIL"), # 非数字字符串
(-100, "SUCCESS"), # 负数(注意:某些系统可能返回 SUCCESS 但 data 为 null,这里预期 SUCCESS)
(666666666666666666666666666666666666666666666666666666666666, "FAIL") # 超长数字(超出整数范围)
])
def test_getAuthorInfo_fail(self, blogId, code):
"""
已登录状态下,使用异常/非法的 blogId 参数获取作者信息
验证接口对异常输入的处理是否正确
"""
# 读取 token
token = read_yaml("data.yml", "user_token_header")
# 构造认证头
header = {
"user_token_header": token
}
# 直接拼接查询参数到 URL 中(也可使用 params 参数,但这里用了字符串拼接)
url = self.url + "?blogId=" + str(blogId)
# 发送 GET 请求(注意:未使用 params,而是将参数写在 URL 中)
r = Request().get(url=url, headers=header)
# 校验 JSON 结构
validate(r.json(), self.schema)
# 断言业务状态码符合参数化传入的预期值(SUCCESS 或 FAIL)
assert r.json()["code"] == code
11. 项目依赖清单 (requirements.txt)
pytest==8.3.2
allure-pytest==2.13.5
jsonschema==4.23.0
PyYAML==6.0.1
5. 测试执行结果
5.1 总体统计
| 指标 |
数值 |
| 总用例数 |
36 |
| 通过数 |
36 |
| 失败数 |
0 |
| 阻塞数 |
0 |
| 通过率 |
100% |
5.2 详细结果
5.2.1 登录接口 (/user/login)
| 场景 |
预期 |
实际 |
结果 |
| 正确账号密码 |
SUCCESS + token |
符合 |
✅ |
| 密码错误 |
FAIL / 密码错误 |
符合 |
✅ |
| 用户不存在 |
FAIL / 用户不存在 |
符合 |
✅ |
| 账号为空 |
FAIL / 账号不能为空 |
符合 |
✅ |
| 密码为空 |
FAIL / 密码不能为空 |
符合 |
✅ |
| 超长账号 |
FAIL / 用户不存在 |
符合 |
✅ |
| GET 方法 |
SUCCESS(实际支持) |
符合 |
✅ |
5.2.2 博客列表接口 (/blog/getList)
| 场景 |
预期 |
实际 |
结果 |
| 未登录访问 |
401 |
401 |
✅ |
| 已登录访问 |
SUCCESS + 数组 |
符合 |
✅ |
| POST 方法 |
SUCCESS +实际支持 |
符合 |
✅ |
5.2.3 发布博客接口 (/blog/add)
| 场景 |
预期 |
实际 |
结果 |
| 未登录 |
401 |
401 |
✅ |
| 标题内容完整 |
data=true |
符合 |
✅ |
| 标题为空 |
data=false |
符合 |
✅ |
| 内容为空 |
data=false |
符合 |
✅ |
| 包含 Markdown 图片 |
data=true |
符合 |
✅ |
5.2.4 博客详情接口 (/blog/getBlogDetail)
| 场景 |
预期 |
实际 |
结果 |
| 无 token |
401 |
401 |
✅ |
| 有效 token + 合法 blogId |
SUCCESS + 详情 |
符合 |
✅ |
| blogId 不存在 |
FAIL |
符合 |
✅ |
| blogId 为字符串 |
FAIL |
符合 |
✅ |
5.2.5 用户信息与作者信息接口
- 所有认证接口未携带 token → 返回 401 ✅
- 携带正确 token → 返回完整用户信息,通过 JSON Schema 校验 ✅
6. 缺陷分析
6.1 发现的缺陷
| 缺陷ID |
接口 |
问题描述 |
严重程度 |
状态 |
| BUG-001 |
/blog/getBlogDetail |
传入不存在的 blogId 返回 "内部错误, 请联系管理员",而非友好提示 |
中 |
待评审 |
| BUG-002 |
/user/login |
GET 方法也能成功登录,违反 RESTful 规范(应只允许 POST) |
低 |
待评审 |
6.2 改进建议
- 对不存在资源返回 404 及更明确的错误码,避免 "内部错误"
- 强制登录接口仅接受 POST 方法,提高安全性
7. 测试结论
✅ 接口核心功能稳定 ,正常流程全部通过。
⚠️ 异常处理存在少量不友好提示,建议优化。
🔐 认证机制有效,未授权访问被正确拦截。
📊 自动化测试覆盖率达到预期,可纳入 CI 流水线。
最终结论: 博客系统接口基本满足上线要求。
8. 附件
8.1 测试执行日志
allure generate .\allure-results\ -o .\allure-report --clean
8.2 自动化项目代码仓库
Liull/Automation