一、前置准备:项目上线
说明 :本项目用 SQLite(chatDB.db),启动时会自动建表,没有独立 MySQL/PostgreSQL。第 5 步主要是确认库文件路径与写权限。
1.准备服务器
本项目上线到阿里云服务器。
系统配置:
- 操作系统:Ubuntu22.04
- CPU:2核
- 内存:2G
- 公网带宽:3Mbps
开放防火墙端口:8000(项目要监听8000端口)

2.拉取项目代码
以HTTPS形式拉取gitee远程仓库代码。在Linux机器上新建一个目录,专门存放拉取的代码。比如我的目录是/home/madison/workplace
shell
git clone https://gitee.com/Madison-No7/ai_model_acess.git
3.安装依赖
构建工具:cmake、g++、make
- 库:OpenSSL、jsoncpp、spdlog、fmt、gflags、sqlite3、
- 头文件:含
httplib.h
该项目所需软件依赖:
shell
sudo apt update
sudo apt install -y \
build-essential cmake pkg-config \
libssl-dev zlib1g-dev libbrotli-dev \
libcpp-httplib-dev \
libsqlite3-dev \
libjsoncpp-dev \
libspdlog-dev libfmt-dev \
libgflags-dev \
libcrypt-dev
4.编译项目
进入ai_model_acess/ai_acess/chatserver/build目录下:cmake ..构建项目,然后再make -j$(nproc)编译,成功后会生成可执行文件 AIChatServer,并自动把 ChatServer.conf 拷到 build/。
shell
cd ai_model_acess/ai_acess/chatserver/build
cmake ..
make -j$(nproc)
5.配置数据库
本项目是嵌入式 SQLite,不是单独装库服务。所以不需要配置数据库,无需手动执行 SQL 初始化脚本;首次启动自动建表。
注意 :服务用户要对 build/ 目录可写,因为chatDB.db在bulid目录下。
6.systemd 托管 AIChatServer
创建/etc/systemd/system/ai-chat-server.service文件,配置后端服务(systemd)。
systemd 服务配置:
目的 :SSH 断开后服务仍运行,开机自启。
ai-chat-server.server:
shell
[Unit]
Description=AI Chat Server (AIChatServer)
Documentation=file:///home/madison/workplace/ai_model_acess/README.md
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=madison
Group=madison
# 必须在 build 目录下运行:相对路径依赖 ./www、chatDB.db、ChatServer.conf
WorkingDirectory=/home/madison/workplace/ai_model_acess/ai_acess/chatserver/build
ExecStart=/home/madison/workplace/ai_model_acess/ai_acess/chatserver/build/AIChatServer --config_file=ChatServer.conf
# API Key 等敏感配置,安装时复制 env.example 到此路径并填写
EnvironmentFile=/etc/ai-chat-server.env
Restart=on-failure
RestartSec=5
TimeoutStopSec=15
KillMode=mixed
# 基础加固
NoNewPrivileges=true
PrivateTmp=true
# 日志走 journald(程序当前写 stdout)
StandardOutput=journal
StandardError=journal
SyslogIdentifier=ai-chat-server
[Install]
WantedBy=multi-user.target
读取环境文件:/etc/ai-chat-server.env,里面保存了api_key。
执行以下命令就可以管理ai-chat-server服务了:
shell
systemctl start ai-chat-server #启动
systemctl status ai-chat-server # 看状态
systemctl restart ai-chat-server # 改代码重新 make 后重启
systemctl enable ai-chat-server #Linux开机自启
systemctl stop ai-chat-server # 停止
journalctl -u ai-chat-server -f # 看日志
7.测试上线
本机进程和监听端口检查:
查看后端进程是否存在:ps -ef | grep AIChatServer

查看是否监听8000端口:ss -tlnp | grep 8000

访问Web服务和功能验证:
直接在浏览器输入网址:服务器地址:8000访问。

查看项目数据库:
查看SQLite数据库:
shell
#安装命令行客户端 sqlite3,用来交互式打开、查询数据库。
sudo apt install sqlite3
cd /home/madison/workplace/ai_model_acess/ai_acess/chatserver/build
#进入chatDB.db数据库
sqlite3 chatDB.db
SQLite操作:
SQLite
.tables -- 查看所有表
.schema users -- 查看某表结构
SELECT * FROM users;
.quit -- 退出
二、项目测试
1.文档信息:
| 项目 | 内容 |
|---|---|
| 项目名称 | 多模型流式智能对话系统 |
| 测试版本 | V1.0 |
| 测试人员 | Madison |
| 测试时间 | 2026-09-25 ~ 2026-09-26 |
| 测试类型 | 功能测试、接口测试、性能测试 |
| 报告版本 | V1.0 |
2.项目概述
系统背景:
该项目是基于 C/C++ 研发的多模型智能聊天助手,项目自研底层通信 SDK,统一接入了 QianWen/DouBao/DeepSeek 等主流云端大模型及本地离线模型,并配合 Web 端实现了支持流式输出与上下文记忆的对话服务。
主要业务:
- 支持用户登录注册
- 支持用户会话历史信息管理
- 支持自动选择聊天大模型
- 支持大模型流式输出
- 支持上下文记忆对话服务
3.测试环境
3.1手动测试环境
| 类别 | 配置 |
|---|---|
| 硬件 | 拯救者Y7000 2019 PG0 |
| CPU | Intel Core |
| 内存 | 16GB |
| 浏览器 | Microsoft Edge 版本153.0.4234.48 |
| 操作系统 | Win10家庭版 |
| 抓包工具 | Charles |
3.2 Web UI自动化测试环境
| 类别 | 配置 |
|---|---|
| 辅助开发工具 | Cursor |
| 自动化测试工具 | selenium==4.0.0 webdriver-manager==4.0.2 pytest==8.3.2 allure-pytest==2.13.5 faker==26.1.0 pytest-order==1.3.0 PyYAML==6.0.1 |
| 测试脚本语言 | Python |
| 自动化测试浏览器 | Google Chrome 154.0.8037.93 (正式版本) |
| 持续集成工具 | Jenkins |
3.3 接口自动化测试环境
| 类别 | 配置 |
|---|---|
| 辅助测试工具 | Postman,Postman-MCP |
| 开发工具 | Cursor |
| 自动化测试工具 | allure-pytest==2.13.5 jsonschema==4.23.0 pytest==8.3.2 PyYAML==6.0.1 faker==26.1.0 requests==2.32.3 |
| 持续集成工具 | Jenkins |
3.4 性能测试环境:jmeter
4.测试范围
| 业务模块 | 接口自动化 | UI 自动化 | 说明 |
|---|---|---|---|
| 用户注册 | ✅ 成功 / 失败 | ✅ 跳转 + YAML 注册 | 校验、空字段、成功注册等 |
| 用户登录 | ✅ 成功 / 失败 | ✅ 跳转 + YAML 登录 | 错误账号、成功登录加载用户会话信息 |
| 用户登出 | ✅ 成功 / 失败 | ✅ 登出后显示「登录」 | 接口含无效 Cookie 等 |
| 模型列表 | ✅ 成功 / 失败 | ✅ 选模型时间接覆盖 | UI 不单独测列表接口 |
| 创建会话 | ✅ 成功 / 失败 | ✅ 侧边栏 / 顶栏建对话 | 含未选模型提示 |
| 会话列表 | ✅ 成功 / 失败 | ✅ 数量、切换活跃会话 | UI 断言条数=2 等 |
| 重命名会话 | ✅ 成功 / 失败 | ✅ 列表重命名 / 顶栏重命名 | |
| 切换模型 | ✅ 成功 / 失败 | ✅ 对话中下拉切模型 | |
| 删除会话 | ✅ 成功 / 失败 | ✅ 侧边栏删到空列表 | |
| 会话历史 | ✅ 成功 / 失败 | ✅发消息后页面可见历史 | |
| 异步发消息 | ✅ SSE 成功 / 失败 | ✅ 发送、回复中锁定、气泡 | |
| 会话搜索 | ❌ | ✅ 侧边栏 / 顶栏,有/无匹配 | 偏前端交互 |
| 静态资源 | ✅ 成功 / 失败 | ❌ | 页面/资源路径类 |
| 协议与安全 | ✅ security_protocol |
❌ | 方法/协议/异常请求等 |
5.测试设计
1)登录弹窗测试用例设计:
测试用例设计模版:

提示词:
text
根据提供的UI原型图,帮我设计登录弹窗的测试用例
要求:
1.测试用例设计从功能测试、界面测试、性能测试、兼容性测试、易用性测试、安全测试这几个方面去设计
2.尽可能多的覆盖UI原型图
3.严格按照我提供的测试用例模版图设计
4.只设计优先级最高的测试用例
最后输出的测试用例需要有严格的层级关系,先能在md格式中按照层级关系显示,最后才能导入XMind中

2)注册弹窗测试用例设计:
Bug:系统不能限制简单密码的注册,如使用密码123456能注册成功。
测试点设计模版:

提示词:
根据提供的UI原型图,帮我设计注册弹窗的测试点
要求:
1.测试用例设计从功能测试、界面测试、性能测试、兼容性测试、易用性测试、安全测试这几个方面去设计
2.尽可能多的覆盖UI原型图
3.严格按照我提供的测试点模版图设计
4.只设计优先级最高的测试点
最后输出的测试点需要有严格的层级关系,先能在md格式中按照层级关系显示,最后才能导入XMind中

3)登出测试

提示词:
根据提供的UI原型图,帮我设计退出登录的测试点
要求:
1.测试用例设计从功能测试、界面测试、性能测试、兼容性测试、易用性测试、安全测试这几个方面去设计
2.尽可能多的覆盖UI原型图
3.严格按照我提供的测试用例模版图设计
4.只设计优先级最高的测试点
最后输出的测试点需要有严格的层级关系,先能在md格式中按照层级关系显示,最后才能导入XMind中

4)前端主页测试
提示词:
请你根据提供的UI原型图,为主页面中的所有元素设计测试点,要求从功能测试、界面测试、易用性测试、安全测试、兼容性测试、性能测试这几个方面去测试,只设计优先级高的测试点,最后最后输出的测试点需要有严格的层级关系,先能在md格式中按照层级关系显示,最后才能导入XMind中

5)会话与权限测试

6)对话与流式输出测试

6.测试执行
Bug汇总:
Bug1:
- 缺陷标题:【登录】暴力破解防护
- 缺陷出现的版本:ai_acess_model V1.0
- Bug的严重程度:严重
- Bug优先级:P1
- 前提条件(可选):
- 打开浏览器,输入项目网址,进入主页
- 测试步骤:
- 点击主页中的"去登录"按钮,进入到登录弹窗。
- 输入正确的账号,错误的密码,连续点击"登录"按钮50次。
- 预期结果:用户名正确的输入密码错误,连续5次进行登录请求后,提醒用户:"你以密码错误尝试登录第5次,请2小时后再登录!"
- 实际结果:
- 连续登录了50次,都没有触发次数限制。
- 附件信息(可选):

Bug2:
- 缺陷标题:断网用户登出测试
- 缺陷出现的版本:ai_acess_model V1.0
- Bug的严重程度:次要
- Bug优先级:P2
- 前提条件(可选):
- 打开浏览器,输入项目网址,进入主页
- 测试步骤:
- 点击主页中用户菜单按钮,接着点击"退出登录"按钮。
- 预期结果:
- 点击"退出登录"按钮后,页面回到未登录的初始状态(会话列表为空)。
- 实际结果:
- 点击登出后,页面未回到为未登录的样子,会话列表里面还有用户会话信息。
- 附件信息(可选):

Bug3:
- 缺陷标题:登录信息安全测试
- 缺陷出现的版本:ai_acess_model V1.0
- Bug的严重程度:严重
- Bug优先级:P1
- 前提条件(可选):
- 打开浏览器,输入项目网址,进入主页
- 测试步骤:
- 点击主页中的"去登录"按钮,进入到登录弹窗。
- 输入正确的账号,正确的密码,点击登录。
- 预期结果:
- 用户输入的用户名和密码在发送请求数据,传输的应是加密的用户名和密码。
- 实际结果:
- 没有对数据加密,用户密码为明文传输,存在安全风险。
- 附件信息(可选):

三、UI自动化测试
脚本源码:
https://gitee.com/Madison-No7/wub_-ui_test_for_-aichat-project.git
项目结构:
wub_-ui_test_for_-aichat-project/
├── pytest.ini # 收集 script 下三份用例、pythonpath、Allure 目录、中文 case id
├── requirement.txt # 依赖:selenium / webdriver-manager / pytest / allure / faker / PyYAML 等
├── run.py # 一键执行:pytest → allure generate → allure open
├── config.py # TEST_URL、BROWSER_TYPE、Faker 占位符替换(${faker.username} 等)
├── tools.py # 驱动单例、滚动日志、读 YAML(DriverTools / GetLog / YamlUtil)
├── README.md / README.en.md # 安装、运行、Jenkins / Allure 说明
│
├── base/ # 页面基类(PO 公共操作)
│ └── page_base.py # 打开 URL、显式等待定位、点击/输入、截图、PageError
│
├── page/ # 页面对象层(元素定位 + 业务动作)
│ ├── page_register.py # 注册弹窗:填表、提交、跳转登录
│ ├── page_login.py # 登录弹窗:登录/失败提示、跳转注册、用户菜单
│ └── page_chat.py # 聊天主界面:建会话、选模型、发消息、侧边栏、搜索/删除等
│
├── script/ # pytest 用例层(pytest-order 控制顺序)
│ ├── conftest.py # 浏览器生命周期,session 级 Fixture:整场只开一次浏览器并打开 TEST_URL,结束时退出
│ ├── test_register.py # order 1--2:跳转登录 + YAML 注册(先失败后成功)
│ ├── test_login.py # order 3--4:跳转注册 + YAML 登录
│ └── test_chat.py # order 5+:会话/模型/消息等(setup 时 API 清会话)
│
├── data/ # YAML 数据驱动
│ ├── register_test_data.yaml # 注册用例(含 Faker 占位符)
│ ├── login_test_data.yaml # 登录用例(空用户名、错误密码等)
│ └── test.yaml # 临时/调试用 session_id,不参与主流程
│
├── log/ # 运行日志(tools.GetLog 写入)
│ ├── all_test_info.log
│ ├── info_test_info.log
│ └── error_test_info.log
│
├── allure-results/ # Allure 原始结果(pytest.ini --alluredir)
└── allure-report/ # python run.py 生成并打开的 HTML 报告
测试报告:

做UI自动化测试所遇问题:
- 使用pytest参数化读取yaml文件的数据?
- 用pytest提供的conftest.py配置文件实现当前目录下的全局前后置,即整个自动化测试,前置:浏览器启动,并打开URL,后置:整个自动化测试结束后,关闭浏览器。
- 如何在yaml文件中使用faker造的数据
- 断言已报错,函数后面的代码就不会执行了,直接跳出函数。
read_yaml函数实现的功能:
关于read_yaml函数:将yaml格式的数据解析成Python格式,同时将yaml数据中的占位符替换成faker自动构造的数据。
read_yaml可以处理的yaml数据格式:列表、列表嵌套字典、字典、字典嵌套字典。
列表里面嵌套字典的yaml数据:
yaml
- id: "用户名为空注册"
username: ""
password: ${faker.password}
confirm_password: ${password}
nickname: ${faker.name}
expected_result: "请填写所有字段"
- id: "注册成功"
username: ${faker.username}
password: ${faker.password}
confirm_password: ${password}
nickname: ${faker.name}
expected_result: "注册成功,请登录"
字典里面嵌套字典的yaml数据:
yaml
0:
id: "用户名为空注册"
username: ""
password: ${faker.password}
confirm_password: ${password}
nickname: ${faker.name}
expected_result: "请填写所有字段"
1:
id: "注册成功"
username: ${faker.username}
password: ${faker.password}
confirm_password: ${password}
nickname: ${faker.name}
expected_result: "注册成功,请登录"
字典格式的yaml数据:
yaml
data-session-id: session_179065511800000075
data-session-id: session_179065511800000076
具体实现:
python
@staticmethod
def read_yaml(path):
# str(Path(__file__).parent) --- 当前文件的父目录
filepath = str(Path(__file__).parent) + "/data/" + path + ".yaml"
with open(filepath, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)
cases = list(data.values()) if isinstance(data, dict) else data
#只有当cases[0]是字典时,才遍历cases
#替换
if isinstance(cases[0], dict):
for case in cases:
# case.items() 获取case中的所有键值对,key是字典的键,value是字典的值,返回一个迭代器
for key, value in case.items():
if value == "${password}":
# 如果value是${password},则将value替换为config中的password
case[key] = case["password"]
else:
# 如果value不是${password},则将value替换为config中的placeholder
case[key] = config.resolve_placeholder(value)
return data
resolve_placeholder函数:
python
def resolve_placeholder(value):
"""将 Faker 占位符替换为随机数据"""
if value == "${faker.username}":
return fake.user_name()
if value == "${faker.password}":
return fake.password()
if value == "${faker.name}":
return fake.name()
return value
- data = yaml.safe_load(f):把 YAML 转成 Python 数据结构。
比如:
YAML
- id: 登录成功
username: admin
password: 123456
- id: 密码错误
username: admin
password: 111111
读取后:
python
data = [
{
"id":"登录成功",
"username":"admin",
"password":"123456"
},
{
"id":"密码错误",
"username":"admin",
"password":"111111"
}
]
cases = list(data.values()) if isinstance(data, dict) else data:如果 data 最外层是字典:cases为list(data.values()),否则为data。
data.values()的返回值是一个dict_values类型的可迭代对象,里面存放字典所有的 value(值)。list(data.values())将data.values()转换为列表。
YAML是列表里面嵌套字典:
比如:
YAML
- id: "用户名为空注册"
username: ""
password: ${faker.password}
confirm_password: ${password}
nickname: ${faker.name}
expected_result: "请填写所有字段"
- id: "注册成功"
username: ${faker.username}
password: ${faker.password}
confirm_password: ${password}
nickname: ${faker.name}
expected_result: "注册成功,请登录"
因为YAML是列表,所以cases为data,即将YAML数据读取成Python格式的数据
python
[
{
"id": "用户名为空注册",
"username": "",
"password": "${faker.password}",
"confirm_password": "${password}",
"nickname": "${faker.name}",
"expected_result": "请填写所有字段"
},
{
"id": "注册成功",
"username": "${faker.username}",
"password": "${faker.password}",
"confirm_password": "${password}",
"nickname": "${faker.name}",
"expected_result": "注册成功,请登录"
}
]
YAML是字典里面嵌套字典:
比如:
YAML
0:
id: "用户名为空注册"
username: ""
password: ${faker.password}
confirm_password: ${password}
nickname: ${faker.name}
expected_result: "请填写所有字段"
1:
id: "注册成功"
username: ${faker.username}
password: ${faker.password}
confirm_password: ${password}
nickname: ${faker.name}
expected_result: "注册成功,请登录"
因为YAML是字典,所以cases是data.values(),即获取字典中所有的值(value,去掉key,保留value),返回一个可迭代对象。
data:
python
{
"0": {
"id": "用户名为空注册",
"username": "",
"password": "${faker.password}",
"confirm_password": "${password}",
"nickname": "${faker.name}",
"expected_result": "请填写所有字段"
},
"1": {
"id": "注册成功",
"username": "${faker.username}",
"password": "${faker.password}",
"confirm_password": "${password}",
"nickname": "${faker.name}",
"expected_result": "注册成功,请登录"
}
}
data.values():既不是list也不是dict
python
dict_values(
[
{
"id": "用户名为空注册",
"username": "",
"password": "${faker.password}",
"confirm_password": "${password}",
"nickname": "${faker.name}",
"expected_result": "请填写所有字段"
},
{
"id": "注册成功",
"username": "${faker.username}",
"password": "${faker.password}",
"confirm_password": "${password}",
"nickname": "${faker.name}",
"expected_result": "注册成功,请登录"
}
]
)
转换成列表:list(data.values())
python
[
{
"id": "用户名为空注册",
"username": "",
"password": "${faker.password}",
"confirm_password": "${password}",
"nickname": "${faker.name}",
"expected_result": "请填写所有字段"
},
{
"id": "注册成功",
"username": "${faker.username}",
"password": "${faker.password}",
"confirm_password": "${password}",
"nickname": "${faker.name}",
"expected_result": "注册成功,请登录"
}
]
YAML是普通的字典:
YAML
data-session-id: session_179065511800000076
因为YAML是字典,所以cases是data.values()。
data.values:
python
dict_values(
['session_179065511800000076']
)
list(data.values()):
python
['session_179065511800000076']
- dict.items():获取字典中所有的 键(key) 和 值(value) 对,返回一个可迭代对象。
四、接口自动化测试
脚本源码:
接口自动化测试项目地址: https://gitee.com/Madison-No7/aichat-project-apitest
项目结构:
aichat-project-apitest/
├── conftest.py # 全局 Fixture:登录 Cookie、本用户/他人/空会话、可删/已删会话、已登出 Cookie
├── pytest.ini # 收集规则、pythonpath、Allure 目录、中文 case id、markers
├── requirements.txt # 依赖:pytest / requests / PyYAML / jsonschema / faker / allure-pytest
├── run.py # 一键执行:pytest + allure generate
├── config.py # Base_Url、接口路径、测试账号(对应模板的 config/)
├── README.md / README.en.md # 使用说明
│
├── scripts/ # pytest 用例层(对应模板 testcases/)
│ ├── test_user_register.py # 注册
│ ├── test_uer_login.py # 登录
│ ├── test_user_logout.py # 登出
│ ├── test_get_modellist.py # 模型列表
│ ├── test_create_session.py # 创建会话
│ ├── test_get_sessionlist.py # 会话列表
│ ├── test_rename_session.py # 重命名会话
│ ├── test_switch_model.py # 切换模型
│ ├── test_delete_session.py # 删除会话
│ ├── test_session_history.py # 会话历史
│ ├── test_send_messageasync.py # 异步发消息(SSE)
│ ├── test_static_resources.py # 静态资源
│ └── test_security_protocol.py # 协议 / 安全
│
├── data/ # YAML 数据驱动(扁平;成功/失败拆文件)
│ ├── user_register_success.yaml | user_register_failed.yaml
│ ├── user_login_success.yaml | user_login_failed.yaml
│ ├── user_logout_success.yaml | user_logout_failed.yaml
│ ├── get_model_list_success.yaml | get_model_list_failed.yaml
│ ├── create_session_success.yaml | create_session_failed.yaml
│ ├── get_sessions_success.yaml | get_sessions_failed.yaml
│ ├── rename_session_success.yaml | rename_session_failed.yaml
│ ├── switch_model_success.yaml | switch_model_failed.yaml
│ ├── delete_session_success.yaml | delete_session_failed.yaml
│ ├── get_sessionhistory_success.yaml | get_sessionhistory_failed.yaml
│ ├── send_messageasync_success.yaml | send_messageasync_failed.yaml
│ ├── get_static_resources_success.yaml | get_static_resources_failed.yaml
│ └── security_protocol.yaml # 安全用例合在一份
│
├── schema/ # 接口返回 JSON Schema({接口}_success.json | _failed.json)
│ ├── user_register/
│ ├── user_login/
│ ├── user_logout/
│ ├── get_modellist/
│ ├── create_session/
│ ├── get_sessionlist/
│ ├── rename_session/
│ ├── switch_model/
│ ├── delete_session/
│ ├── session_history/
│ ├── send_messageasync/
│ └── security_protocol/
│
├── utils/ # 公共能力 + 工具
│ ├── request_util.py # requests 封装(GET/POST/PATCH/DELETE + 请求响应日志)
│ ├── assert_response.py # 状态码 / message / success / Schema / SSE / 安全断言
│ ├── schema_util.py # 按接口名加载 schema/*.json
│ ├── logger_util.py # 日志写入 log/
│ ├── yaml_util.py # 读 YAML;收集阶段 static_replace
│ ├── data_replace_util.py # 静态替换 + 运行时填 ${login_session_id} 等
│ ├── data_generate_util.py # Faker 造数;登录 / 建会话 / 删除 / 登出(供 Fixture)
│ └── build_request_data.py # 从 YAML 拼 cookie、header、body、URL
│
├── docs/ # 人工文档
│ ├── 接口文档.md
│ ├── 接口实际响应.md
│ ├── 接口测试用例模版.md
│ └── 接口测试用例-postman.md
│
├── log/ # 运行日志(按天滚动)
│ ├── all_test_info.log
│ └── info_test_info.log
│
├── allure-results/ # Allure 原始结果(pytest.ini --alluredir)
└── allure-report/ # python run.py 生成的 HTML 报告
测试报告:

AI辅助生成测试用例:
请你根据@docs/接口文档.md ,为接口设计测试用例,测试用例模版@aichat-project-apitest/docs/接口测试用例模版.md ,设计的测试用例要求覆盖正常情况、边界情况、异常输入、安全测试场景。并将生成的 测试用例写入到 接口测试用例-postman.md文档中,其中预期结果和实际结果不填写。
1)将测试用例添加到postman执行:
请你根据@aichat-project-apitest/docs/接口测试用例-postman.md ,使用postman-mcp-server,将@aichat-project-apitest/docs/接口测试用例-postman.md 中的所有测试用例,分接口添加到AIChaPostmanTest集合中,没有集合创建集合,其中login_session_id需要从登录接口获取,own_session_id需要从会话列表接口返回值获取,并将它们设置为集合变量,作为其他接口的请求参数,添加完测试用例,并运行所有测试用例。
请你将@aichat-project-apitest/docs/接口实际响应.md 中每个测试用例的message、success字段,以及响应状态码写回到@aichat-project-apitest/docs/接口测试用例-postman.md 中预期结果那一列
2)将测试用例中的请求和预期响应数据写到yaml文件
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中注册接口测试用例中的请求体参数与请求体参数和预期响应值转换成yaml格式写入到@aichat-project-apitest/data/user_register_success.yaml 中,参考已写入的yaml数据
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中注册接口失败测试用例中的请求体参数与请求体参数和预期响应值转换成yaml格式写入到@aichat-project-apitest/data/user_register_failed.yaml 中,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中登录接口测试用例中成功登录的请求体参数与请求体参数和预期响应值转换成yaml格式写入到@aichat-project-apitest/data/user_login_success.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中登录接口测试用例中登录失败的请求体参数与请求体参数和预期响应值转换成yaml格式写入到@aichat-project-apitest/data/user_login_failed.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中获取模型列表成功接口测试用例中的请求体参数与请求体参数和预期响应值转换成yaml格式写入到@aichat-project-apitest/data/get_model_list_success.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中获取模型列表失败接口测试用例中的请求体参数与请求体参数和预期响应值转换成yaml格式写入到@aichat-project-apitest/data/get_model_list_failed.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中获取模型列表失败接口测试用例中的请求体参数与请求体参数和预期响应值转换成yaml格式写入到@aichat-project-apitest/data/get_model_list_failed.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中创建会话接口测试用例中成功创建的请求体参数与请求体参数和预期响应值转换成yaml格式写入到@aichat-project-apitest/data/create_session_success.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中创建会话接口测试用例中失败创建的请求体参数与请求体参数和预期响应值转换成yaml格式写入到@aichat-project-apitest/data/create_session_failed.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中获取会话列表测试用例中成功获取和失败获取的请求体参数与请求体参数和预期响应值转换成yaml格式分别写入到@aichat-project-apitest/data/get_sessions_success.yaml 和@aichat-project-apitest/data/get_sessions_failed.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中切换模型测试用例中成功切换和失败切换的请求体参数与请求体参数和预期响应值转换成yaml格式分别写入到@aichat-project-apitest/data/switch_model_success.yaml 和@aichat-project-apitest/data/switch_model_failed.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中登出用例中成功登出和失败登出的请求体参数与请求体参数和预期响应值转换成yaml格式分别写入到@aichat-project-apitest/data/user_logout_success.yaml 和@aichat-project-apitest/data/user_logout_failed.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
请你将@aichat-project-apitest/docs/接口测试用例-postman.md 中获取静态资源用例中成功获取和失败获取的请求头参数与请求体参数和预期响应值转换成yaml格式分别写入到@aichat-project-apitest/data/get_static resources_success.yaml 和@aichat-project-apitest/data/get_static resources_failed.yaml 中,没有该文件就创建,参考@aichat-project-apitest/data/user_register_success.yaml
做接口自动化测试中所遇问题:
1.对于写测试脚本的问题:
对于测试注册失败接口问题,测试用例中请求数据各不相同,但是响应体的结构是一样的。应该如何发送请求?
响应体结构:
{
"message": "用户名已存在",
"success": false
}
请求信息:请求体需至少包含两个字段username和password,nickname可以不要。
- 请求体参数变化,不一定需要三个字段,有的需要nickname字段,有的不需要,有的又缺失passwrd字段
解决:有哪些业务字段,请求头里就带哪些字段,循环查找这一条用例中的key是否在("username","password","nickname")中,如果在,就将key=value添加到request_data中。
python
"""字典推导式"""
request_data = {
key: data[key]
for key in ("username", "password", "nickname")
if key in data
}
- 有的测试用例中指明content_type: application/x-www-form-urlencoded
解决:还是业务字段,先看测试用例中有没有content_type字段(data.get("content_type"))且不为空,就将其提取出来,放到请求头中。
python
request_headers = {}
# data.get()字典安全获取值:有这个键就返回值,没有也不报错,默认返回 None。
if data.get("content_type"):
request_headers["Content-Type"] = data["content_type"]
现在已有请求头和请求体了,能发送POST请求了。接下来就是应对各种请求方式,按"请求形态"选发送方式
- 有 body 字符串? → data=原文(非 JSON / 表单)
- request_data 为空? → data=""(空 body)
- content_type 键在且值为空? → data=json.dumps(...)(有 JSON 文本但不自动加头)
- 其余 → json=request_data(普通 JSON)
- 请求体是JSON/表单格式
- 没有请求体,请求头直接发请求
- 请求头为空,比如content_type: "",但请求体不为空
python
# 按请求类型发送
if "body" in data:
# 非 JSON ,表单原文
response = request_util.post_request(
self.url,
headers=request_headers or None,
data=data["body"],
)
# body字段为空,没有data和json字段
elif not request_data:
# 空请求体
response = request_util.post_request(
self.url,
headers=request_headers or None,
data="",
)
# 有content_type字段,但值为空
elif "content_type" in data and not data["content_type"]:
# 有 JSON 字段,但故意不带 Content-Type(不能用 json=,否则会自动补上)
response = request_util.post_request(
self.url,
data=json.dumps(request_data),
)
else:
# 普通 JSON 用例
response = request_util.post_request(
self.url,
headers=request_headers or None,
json=request_data,
)
所以在发送请求时,需要应对各种情况。
字典.get() :
data.get("content_type"),为字典安全取值 ,没有这个key,则返回None,有就返回key所对应的value.
还可以指定默认值:data.get("content_type", "") ,没有该key时,设置key所对应的value为""。
2.对于注册成功后下次注册数据重复的问题:
在成功注册用例中,每执行完一遍测试用例后,下次执行测试时,就会出现错误,因为上一次用户名已经注册过了,会提示用户名已存在,导致结果不符合预期,这是因为成功注册用例使用的是固定值注册,这一次注册完后,下一次就不能用了。
解决办法一: 跑完一遍测试用例后,将数据库中的用户表信息删除。
解决办法二: 使用faker工具构造随机数,每一次注册都不一样。
生成随机用户名函数:
python
import string
import faker
fake = faker.Faker(locale="zh_CN")
#拼接一个字符串:大写+小写字母+0到9数字+下划线+横线
USER_CHARS = string.ascii_letters + string.digits + "_-"
def fake_username(length=None):
#`or` 短路赋值
n = length or fake.random_int(min=3, max=32)
# fake.random_element()从给的集合里随机抽 1 个元素。
# for _ in range(n) 重复抽 n 次
# "".join() 把列表里的每个元素拼接成一个用户名
parts = []
for _ in range(n):
parts.append(fake.random_element(USER_CHARS))
return "".join(parts)
# return "".join(fake.random_element(USER_CHARS) for _ in range(n))
注意 :Python 里这些都算假:None、""、0、False、[]、{}。
3.关于测试数据替换的问题:
read_yaml函数功能 :将yaml数据转换为python格式的数据,如果转换后的数据从最外层来看是字典,可能是测试用例可能是user_cookie,然后取出字典里所有的键值(只有键值,没有键),组成列表(cases),目的:pytest参数化要求列表。
再判断列表的第一个元素是否是字典,如果是字典,那就一定测试用例了,因为只有测试用例是字典嵌套字典的结构,而且测试用例中存在占位符,需要完成替换:循环遍历每一条测试用例(case),然后再case.items()取出一条测试用例中所有的键值对,然后循环遍历键值对,判断是否有占位符需要替换。最终所有占位符替换完后,返回cases(列表)。
对于user_cookie数据源(字典格式),虽然会被转换为列表,但是不会进入替换占位符的循环中,所以最终返回yaml转换为python格式的源数据。
注意 :PyYAML 不允许映射里真正保留重复键,同名键只留最后一次出现的值。所以对于user_cookie数据源,最终只会返回最新的login_session_id,前面那些旧的 login_session_id 都会被覆盖丢掉。
4.关于测试各接口如何做数据替换的问题:
对于获取会话列表的接口 ,测试数据源中有${new_user_login_session_id}以及篡改cookie数据的:"${login_session_id}' OR '1'='1"占位符,read_yaml如何完成替换?
对于${new_user_login_session_id},在替换的时候,会转去新创建一个用户,然后完成登录,获取Cookie,然后以{"new_user_login_session_id": session_id}字典格式写入到user_session.yaml文件中,同时将获取的Cookie完成替换。
对于"${login_session_id}' OR '1'='1",使用 value.replace("${login_session_id}", cookie_data["login_session_id"]替换${login_session_id},这一部分。
对于发送流式消息接口 :
测试数据源中有${own_session_id},表示当前最新登录登录用户创建的会话id,在替换的时候,使用最新登录的login_session_id,去新创建一个会话,并将返回的会话id,写入到user_session.yaml文件中(非必要步骤),因为会多次替换${own_session_id},所以使用全局变量_own_session_id_cache,将当前登录用户创建的会话id缓存起来,下次用,就不用在创建会话了。
测试数据源中还有${other_user_session_id},表示其他用户中的会话id,在替换的时候,转去新创建一个用户,然后登录,之后创建一个会话,将返回的会话id,写入到user_session.yaml文件中(非必要步骤),要使用全局变量_other_user_session_id_cache,将其他用户创建的会话id缓冲起来,方便下次用。
对于获取会话历史接口:
测试数据中有${empty_session_id},表示空消息对话,在替换的时候,使用最新的login_session_id,创建一个会话,将创建的会话id缓存起来。将返回的会话id写入到user_session.yaml文件中(非必要步骤)。
对于删除会话接口:
测试数据中有${deleted_session_id},表示已删除的回话id,在替换的时候,使用最新的login_session_id,创建一个会话,然后再删除刚创建的会话,将删除的会话id缓存起来,方便下次用。将删除的会话id写进user_session表为了在日志、复现、排查时有据可查,这并不是必要的步骤,也并不会再从 user_session.yaml 读回来。
接口自动化测试一般不会指定全局执行顺序,对于破坏性接口 ,比如:删除和登出接口测试,不要使用被人还要用的数据(own_session_id还要在重命名、切模型、发消息等接口中使用,login_session_id要用于接口的校验,几乎所有的接口都会用到),所以对于测试此类接口,需要制造独立的数据来测试,对于删除会话接口,删除成功测试的session_id,使用独立数据${deletable_session_id}作为占位符,替换的时候,转去特意创建一个会话用于删除,可以将该session_id写入user_session.yaml文件,但这不是必要的。
对于登出接口测试,成功登出,会让最新的login_session_id失效,所以对于一些需要做身份校验的接口,在替换的时候,都会转去调用_ensure_login_session_id(),此函数会会用正确的账号密码登录,并返回最新的login_session_id,保证最新的login_session_id是有效的。
5.对一些重复脚本部分做封装
封装schema:
将每个测试函数中的jsonschema数据提取出来,分文件,分类别统一保存到schema文件夹下,然后再封装一个读取json数据的工具类,整段读取json文本(json.load(f)),并返回json对象。

python
import json
from pathlib import Path
SCHEMA_DIR = Path(__file__).parent.parent / "schema"
class SchemaUtil:
@staticmethod
def read_schema(api_name, result_type):
filepath = SCHEMA_DIR / api_name / f"{api_name}_{result_type}.json"
with open(filepath, "r", encoding="utf-8") as f:
return json.load(f)
json.load(f) 读文件,json.loads(f) 读字符串
json.dumps(request_data): 把 Python 字典转成 JSON 字符串。
封装request_data:
因为在每个测试接口函数中,每次请求前都要构造请求数据,所以我想能不能把它抽取出来封装成一个工具函数,专门用于构造请求数据。需要构造请求头、构造cookie、构造请求体、构造url。
封装assert_response:
因为在每个测试接口函数中,得到响应后,都需要做断言,所以我想能不能把它抽取出来封装成一个工具函数专门用于断言响应结果。
数据驱动做法选择:
之前接口测试数据驱动的方式:yaml+read_yaml+data_generate+data_replace
每个测试用例执行前,都会读取 YAML数据源 ,yaml数据源中有很多的占位符,在读取时顺带造数据,并替换为真实的数据,对于一些接口共用的数据,用类变量 + YAML 文件全局缓存起来。对于测试破坏性接口,独立的造数据,不与其他接口共享。(隔离靠的是调用不同的函数)
而现在使用的数据驱动方式:yaml+read_yaml+data_generate+data_replace+conftest
对于每个测试函数,使用pytest参数化读取yaml测试数据源,在读取的时候,只对faker生成的静态测试数据做替换,而对于动态的测试数据(比如login_session_id),是根据接口测试的需要,fixture按 session/function 注入需要的测试数据,也要将注入的数据替换进测试用例。(隔离靠的是fixture)
五、性能测试
测试过程:
- 添加线程组
- 在线程组下添加HTTP请求默认值,因为对于请求接口来说协议、IP地址、端口号、内容编码(utf-8)都是一样的,统一配置在HTTP请求默认值中,HTTP请求取样器中就不用重复写这部分内容了。

- 在线程组下添加多个HTTP请求取样器,在填写上每个接口对应的请求方法、请求路径、请求参数
分别有:
- 获取静态资源
- 注册接口
- 登录接口
- 创建会话
- 获取会话列表
- 流式发消息
- 获取会话历史信息
- 切换模型
- 重命名
- 删除会话
- 用户登出
-
由于JMeter 默认不会自动添加请求头 , 所以需要自行添加HTTP信息头管理器,对于请求方法是post,patch的接口,都需要添加请求头。

-
对于注册接口来说,如果username使用固定值,就无法做到多次反复测试了,所以我选择构造随机数据,这样每次注册都能成功。但是还要保持注册和登录的用户名一样。
我利用内置函数把生成的随机值赋予给一个变量名 (例如
reg_username),在登录接口中直接引用该变量。注册接口请求体:

登录接口请求体:

-
对于登录返回的Cookie还要保存起来,作为其他接口的请求凭证。Jmeter中提供了HTTP Cookie管理器,它会自动帮我管理登录接口返回的Cookie信息,在接口需要时,自动传递。

-
对于发送流式消息、重命名这些接口来说,需要用户的会话id,所以在创建会话时,将返回的会话id提取出来,作为其他的请求参数。由于创建会话返回的会话id在返回的响应体中,为JSON格式,所以使用JSON提取器将会话id提取出来。

-
最后在线程组下添加查看结果树,先用一个线程调试一下这些接口能否正常执行。

通过后,开展梯度压测。
注意:以上接口并没有对接口返回的响应接口做断言,因为接口自动化测试已经测试过没有问题,当然在这里也可以对响应信息做断言。
-
梯度压测
首先添加一个"梯度测试线程组",配置好对应的信息。

梯度压测策略 :一共启动12个线程,等待0秒开始压测,一开始没有线程,下一次在5秒之内增加5个线程,当5个线程启动后, 运行30秒后再启动下一组线程,如此循环启动12个线程,后持续运行80秒,之后1秒释放5个线程。
再将之前搭建的测试接口拷贝到"梯度测试线程组"目录下,添加上一些监视器即可进行梯度压测了。

-
生成性能测试报告
shell
#先创建一个存放测试报告的文件夹,必须是空文件夹,否则会报错
mkdir aichat_test_report
jmeter -n -t aichat-stepping-test.jmx -l aichat-stepping-test.ctl -e -o aichat_test_report/.

测试报告:

jmeter性能测试中遇到的问题:
问题描述:
使用jmeter搭建完项目核心业务流程后,启动执行了一次,但是发现"获取会话历史信息"执行失败。

我仔细核查了该接口的请求头信息,请求Cookie信息都无误,但是抛出了NoHttpResponseException异常,这就很奇怪了,其他接口一切正常,只有"获取会话历史信息"异常,但是接口的请求参数这些都是对的。

为什么会这样呢?
然后我将该接口的请求URL,请求Cookie放到postman上验证,响应成功。
但是jmeter上就是不行,我又不断的尝试,验证,最终定位是连接复用的问题,把流式接口的 Keep-Alive 关闭后,后续接口就一切正常了。
为什么关闭流式接口的keep-alive就一切正常了?
先搞清楚一个概念。
什么是Keep-Alive**?
HTTP 请求完成后不会立即关闭底层 TCP 连接,后续请求可以复用这个连接,从而减少重复建立 TCP 连接的开销。
勾选了Keep-Alive:

流式发消息对话接口的响应体:

客户端:请求头中发送了 Connection: keep-alive。
服务端:HTTP 响应头(Headers)是第一时间发送的 :当客户端向服务端发起流式请求时,服务端一旦接收并校验通过,会先向客户端返回 HTTP 200 及 Header ,框架根据流式特性默认生成并写入了响应头:Connection: keep-alive Keep-Alive: timeout=5, max=5,后端Bug(导致不能复用):但数据流推完([DONE])之后,后端底层逻辑直接结束并强行掐断了底层 TCP 连接(提示 Connection closed)。对于客户端来说,TCP 连接就被无预警地单方面掐断了,客户端并不知道此时服务器已经连接关闭,以为还是保活状态,一复用就会报错。
没勾选Keep-Alive :JMeter 主动发了 Connection: close,服务端识别后被迫响应了 Connection: close,JMeter 识别到 close 标记后便会主动销毁 Socket,下一个接口重新建连,从而顺利通过。
进一步发现:该项目的所有接口响应头中都会包含Connection:close,也就是每次请求响应完后,服务端坚持返回 close,会关闭TCP连接,客户端勾选 Use KeepAlive 仅代表客户端有复用连接的意愿 ,但是服务器不同意,所以勾不勾选 Use KeepAlive,都没有用。以后接口每次请求,都会重新建立TCP连接。

流式接口之后请求报错的根因是什么?
不是长连接本身的问题,而是后端服务端"言行不一"(协议头里宣称保活,流推完却单方面把 Socket 掐死,坑了客户端的连接池)。
如果服务器开启了长连接,但是客户端不想保活,此时听谁的?
在 HTTP/1.1 协议中,连接复用遵循一票否决制 :只有双方同意保活才能保活,只要有一方拒绝,就要不能保活。
因此jmeter中的use keep alive,只对后端允许保活的接口有用。