博客论坛技术迭代
前言:之前我们在 博客系统测试报告 中介绍了Selenium+Jmeter 对系统进行简单的测试,但发现内容不足且代码很繁琐,这次我们结合AI并进入pytest框架进行一些迭代
一.接口自动化测试
1. 接口文档
首先接口文档可以清晰表示对应的接口完整信息(请求方式、URL、Header、Body、请求参数以及对应的响应),这里我们根据需求文档迭代出接口文档,方便进行后续的测试,接口文档如下>:
博客系统接口文档
重点:全局基础配置
1.base_url = http://127.0.0.1:9092
2.用户登录后的userId与user_token作为后续操作(除注册外)的请求头,文中的示例只是作为一个演示
一.注册界面
1.用户注册
接口基本信息>:
| 参数 | 值 |
|---|---|
| 接口名称 | 用户注册 |
| method | post |
| URL | {{base_url}}/user/register |
| 请求类型 | application/x-www-form-urlencoded |
参数信息>:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| username | String | 是 | 登录用户名 | 202691 |
| nickname | String | 是 | 用户昵称 | 202691 |
| password | String | 是 | 登录密码 | 123456 |
| passwordRepeat | String | 是 | 确认密码,必须与password保持一致 | 123456 |
成功响应信息>:
json
状态码>: 200 ok
类型>:application/json;charset=utf-8
{
"code": 0,
"message": "操作成功",
"data": null
}
二.登录界面
1.用户登录
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 用户登录 |
| method | post |
| URL | {{base_url}}/user/login |
| 请求类型 | application/x-www-form-urlencoded |
请求头Header
| 参数名 | 类型 | 是否必填 | 说明 | 示例 |
|---|---|---|---|---|
| username | String | 是 | 用户名 | 202691 |
| password | String | 是 | 用户登录密码 | 123456 |
成功响应信息
json
状态码>:200 ok```json
状态码>:200 ok
类型>:application/json;charset=utf-8
{
"code": 0,
"message": "操作成功",
"data": {
"userId": 25,
"token": "eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs"
}
}
} ```
三.博客列表主页面
1.主页个人信息展示
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 主页个人信息展示 |
| method | get |
| URL | {{base_url}}/user/info |
| 请求类型 | 无 |
请求信息>:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息>:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| id | Long | 否 | 表示要查询用户ID | 83 |
说明 >: 用户登录后userId与token需要从登录接口的返回值主动获取,此处只是一个演示,当真会导致结果不可测
成功响应信息>:
json
状态码>:200 ok```json
状态码>:200 ok
类型>:application/json;charset=utf-8
{
"code": 0,
"message": "操作成功",
"data": {
"id": 25,
"username": "202691",
"nickname": "202691",
"gender": 2,
"avatarUrl": null,
"articleCount": 0,
"isAdmin": 0,
"state": 0,
"createTime": "2026-09-01 11:58:53",
"updateTime": "2026-09-01 11:58:53"
}
}
} } ``` 重点:主页个人信息展示的id参数说明
业务说明>:在博客列表主页,此参数id不用传递,因为获取的是当前的登录人信息;在后续我的帖子界面下面的2因为会去访问别人的帖子首页,可能会去拿着ID获取对应的信息
2.获取导航栏板块信息
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 获取导航栏板块信息 |
| method | get |
| URL | {{base_url}}/board/topList |
| 请求类型 | 无 |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
成功响应信息 >:
json
状态码>:200ok```json
状态码>:200ok
类型>:application/json;charset=utf-8
{
"code": 0,
"message": "操作成功",
"data": [
{
"id": 1,
"name": "Java",
"articleCount": 3,
"sort": 1,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 02:25:55",
"updateTime": "2023-06-25 02:25:55"
},
{
"id": 2,
"name": "C++",
"articleCount": 1,
"sort": 2,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 02:25:56",
"updateTime": "2023-06-25 02:25:56"
},
{
"id": 3,
"name": "前端技术",
"articleCount": 1,
"sort": 3,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 02:25:56",
"updateTime": "2023-06-25 02:25:56"
},
{
"id": 4,
"name": "MySQL",
"articleCount": 1,
"sort": 4,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 11:05:22",
"updateTime": "2023-06-25 11:05:22"
},
{
"id": 5,
"name": "面试宝典",
"articleCount": 0,
"sort": 5,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 11:05:22",
"updateTime": "2023-06-25 11:05:22"
},
{
"id": 6,
"name": "经验分享",
"articleCount": 0,
"sort": 6,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 11:05:22",
"updateTime": "2023-06-25 11:05:22"
},
{
"id": 7,
"name": "招聘信息",
"articleCount": 0,
"sort": 7,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 11:05:22",
"updateTime": "2023-06-25 11:05:22"
},
{
"id": 8,
"name": "福利待遇",
"articleCount": 0,
"sort": 8,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 11:05:22",
"updateTime": "2023-06-25 11:05:22"
},
{
"id": 9,
"name": "灌水区",
"articleCount": 1,
"sort": 9,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 11:05:22",
"updateTime": "2023-06-25 11:05:22"
}
]
}
} ```
3.获取站内信的未读数量
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 获取站内信的未读数量 |
| method | get |
| URL | {{base_url}}/message/getUnreadCount |
| 请求类型 | 无 |
请求信息>:
说明:补充说明信息> 请求信息>:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
成功响应信息>:
json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": 0
}
4.获取站内信信息
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 获取站内信的未读数量 |
| method | get |
| URL | {{base_url}}/message/getAll |
| 请求类型 | 无 |
请求信息>:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
成功响应信息>:
情况一:用户没有发送站内信
json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": []
}
```json 状态码>:200 ok 类型>: { "code": 0, "message": "操作成功", "data": \[\] } ```
情况二:用户收到站内信消息
json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": [
{
"id": 32,
"postUserId": 9,
"receiveUserId": 25,
"content": "你也好呀,这是我给你的回复",
"state": 1,
"createTime": "2026-09-02 01:36:49",
"updateTime": "2026-09-02 01:36:55",
"postUser": {
"id": 9,
"nickname": "test",
"gender": 2,
"avatarUrl": null
}
}
]
}
```json 状态码>:200 ok 类型>: { "code": 0, "message": "操作成功", "data": { "id": 32, "postUserId": 9, "receiveUserId": 25, "content": "你也好呀,这是我给你的回复", "state": 1, "createTime": "2026-09-02 01:36:49", "updateTime": "2026-09-02 01:36:55", "postUser": { "id": 9, "nickname": "test", "gender": 2, "avatarUrl": null } } } ```
5.根据板块ID查询对应板块下的帖子消息
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 根据板块ID查询对应板块下的帖子消息 |
| method | get |
| URL | {{base_url}}/article/getAllByBoardId |
| 请求类型 | 无 |
请求信息>:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数>:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| boardId | Long | 否 | 对应的板块ID 当boardId为空时,处于首页,获取所有的帖子信息; 当boardId为(三.2)下面的正常返回值中 1 - 9数字,会获取对应的板块下的帖子列表 | boardId= boardId=1 |
重点:boardId参数获取建议
注意>:这里你最好从(三.2)接口的返回值拿到boardId作为此处queryString,确保内容连贯性,否则对应板块不存在,获取的结果为>:
json{ "code": 0, "message": "操作成功", "data": [] }成功响应信息>:
情况一 >:用户处于首页,此时BoardId=,且有帖子
json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": [
{
"id": 27,
"boardId": 3,
"userId": 9,
"title": "test",
"visitCount": 8,
"replyCount": 2,
"likeCount": 0,
"state": 0,
"createTime": "2026-03-03 10:45:06",
"updateTime": "2026-03-05 09:53:52",
"own": false,
"user": {
"id": 9,
"nickname": "test",
"avatarUrl": null
}
}
]
}
**情况二** >:用户位于对应板块下,此时boardId为有效ID,比如:boardId=2,且有帖子
json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": [
{
"id": 1,
"boardId": 2,
"userId": 5,
"title": "test",
"visitCount": 19,
"replyCount": 2,
"likeCount": 3,
"state": 0,
"createTime": "2026-02-08 09:09:10",
"updateTime": "2026-03-05 09:57:14",
"own": false,
"user": {
"id": 5,
"nickname": "zhaoliu",
"avatarUrl": null
}
}
]
}
情况三 >:对应的板块下面没有帖子
json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": []
}
6.获取板块的信息
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 根据有效id获取板块的信息 |
| method | get |
| URL | {{base_url}}/board/getById? |
| 请求类型 | 无 |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数 >:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| id | Long | 是 | 对应的板块ID 板块ID为有效id,位于帖子首页不会调用此方法 | id=1 |
成功响应信息 >:
json
状态码>:200 ok```json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": {
"id": 1,
"name": "Java",
"articleCount": 3,
"sort": 1,
"state": 0,
"deleteState": 0,
"createTime": "2023-06-25 02:25:55",
"updateTime": "2023-06-25 02:25:55"
}
}
} ``` 重点:接口鉴权与数据关联说明
- 用户登录接口,登录成功返回 userId、user_token;后续接口 2‑6 全部请求头携带 user_token 做身份鉴权。
- 各接口间的数据关联(如 boardId、articleId)需从上游接口返回值获取,保证测试数据连贯性。
7.用户进行搜索
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 用户进行搜索 |
| method | get |
| URL | {{base_url}}/article/search? |
| 请求类型 | 无 |
请求信息>:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息>:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| searchName | String | 是 | 想要查询的帖子标题 | test |
响应成功的信息>:
json
状态码>:200ok```json
状态码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": [
{
"id": 27,
"boardId": 3,
"userId": 9,
"title": "test",
"visitCount": 8,
"replyCount": 2,
"likeCount": 0,
"state": 0,
"createTime": "2026-03-03 10:45:06",
"updateTime": "2026-03-05 09:53:52",
"own": false,
"user": {
"id": 9,
"nickname": "test",
"avatarUrl": null
}
},
{
"id": 1,
"boardId": 2,
"userId": 5,
"title": "test",
"visitCount": 19,
"replyCount": 2,
"likeCount": 3,
"state": 0,
"createTime": "2026-02-08 09:09:10",
"updateTime": "2026-03-05 09:57:14",
"own": false,
"user": {
"id": 5,
"nickname": "zhaoliu",
"avatarUrl": null
}
}
]
}
] } ``` 说明:补充说明信息
注意:此处若是返回的是空列表,如下>:
json{ "code": 0, "message": "操作成功", "data": [] }> ```json { "code": 0, "message": "操作成功", "data": \[\] } ```
则表示当前无搜索的内容
四.发送帖子界面
1.用户发送帖子
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 用户发送帖子 |
| method | POST |
| URL | {{base_url}}/article/create? |
| 请求类型 | application/x-www-form-urlencoded |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息 >:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| boardId | Long | 是 | 板块ID,描述在哪一个板块下新增内容 | 1 |
| title | String | 是 | 对应帖子标题 | 我是标题 AAA |
| content | String | 是 | 对应帖子内容 | 我是内容 BBB |
成功响应信息>:
json
状态码>:200 ok```json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": **null**
}
"data": **null** } ``` 重点:发帖成功后的数据校验
新增加成功后,要去校验对应板块下的帖子数量接口为 三.2或者主页帖子数量接口为 三.2是否增加#### 四.帖子详情页面
1.根据帖子ID获取详情信息
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 根据帖子ID获取详情信息 |
| method | get |
| URL | {{base_url}}/article/details? |
| 请求类型 | 无 |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息 >:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| id | Long | 是 | 对应的帖子ID | 83 |
重点:关键信息
1.此处的id从三.5接口的返回值获取,此处id=83是因为新增帖子,会自动跳转到主页并调用三.5接口后获取的帖子列表的其中一个id
2.此处id=83是因为新增帖子,会自动跳转到主页并调用三.5接口后获取的帖子列表的其中一个id
2.获取当前帖子的回复列表
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 获取当前帖子的回复信息 |
| method | get |
| URL | {{base_url}}/reply/getReplies? |
| 请求类型 | application/x-www-form-urlencoded |
请求信息>:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息>:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| articleId | Long | 是 | 对应的帖子ID | 83 |
响应成功信息>:
json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": [
{
"id": 72,
"articleId": 83,
"postUserId": 25,
"content": "222222",
"likeCount": 0,
"state": 0,
"createTime": "2026-09-02 04:22:11",
"updateTime": "2026-09-02 04:22:11",
"user": {
"id": 25,
"nickname": "202691",
"avatarUrl": null
}
},
{
"id": 71,
"articleId": 83,
"postUserId": 25,
"content": "123456",
"likeCount": 0,
"state": 0,
"createTime": "2026-09-02 04:12:10",
"updateTime": "2026-09-02 04:12:10",
"user": {
"id": 25,
"nickname": "202691",
"avatarUrl": null
}
}
]
}
3.新增帖子回复(评论)
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 新增帖子回复 |
| method | post |
| URL | {{base_url}}/reply/create? |
| 请求类型 | application/x-www-form-urlencoded |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息 >:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| articleId | Long | 是 | 对应的帖子ID | 83 |
| content | String | 是 | 对应的帖子的回复内容 | 你好呀,9.2号 |
成功响应信息 >:
json
状态码 >:200 ok```json
状态码 >:200 ok
类型 >:
{
"code": 0,
"message": "操作成功",
"data": null
}
"data": null } ``` 重点:关键信息
业务逻辑>:在当前接口内部新增帖子回复后,当前帖子详情页的repleyCount 会加一,然后调用四.2接口同步更新帖子的回复列表
4.帖子评论删除
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 帖子评论删除 |
| method | post |
| URL | {{base_url}}/reply/deleteReplyed? |
| 请求类型 | application/x-www-form-urlencoded |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息 >:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| articleId | Long | 是 | 对应的帖子ID | 86 |
| articleReplyId | Long | 是 | 对应的帖子的回评论ID | 80 |
成功响应信息>:
json
状态码>:200 ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": null
}
5.用户点赞
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 用户点赞 |
| method | post |
| URL | {{base_url}}/article/thumbsUp? |
| 请求类型 | application/x-www-form-urlencoded |
请求信息
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| id | Long | 是 | 对应的帖子ID | 83 |
成功响应信息:
json
状态码>:200ok```json
状态码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": null
}
"data": null } ```
6.用户修改帖子内容
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 用户修改帖子内容 |
| method | post |
| URL | {{base_url}}/article/modify? |
| 请求类型 | application/x-www-form-urlencoded |
请求信息
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| id | Long | 是 | 对应的帖子ID | 83 |
| title | String | 是 | 修改的帖子标题 | 标题我修改了 |
| content | String | 是 | 修改的帖子内容 | 内容我修改了 |
成功响应信息
json
状态码:200 ok```json
状态码:200 ok
类型:
{
"code": 0,
"message": "操作成功",
"data": null
}
"data": null } ``` "data": null } ``` 注意:需谨慎处理
注意>:这里的编辑人ID要是和登录人Id不一样,会提示如下>:
json{ "code": 1006, "message": "不是本人进行改改,修改失败", "data": null }
7.用户删除帖子
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 用户删除帖子 |
| method | post |
| URL | {{base_url}}/article/delete? |
| 请求类型 | application/x-www-form-urlencoded |
请求信息:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| id | Long | 是 | 对应的删除帖子有效ID | 85 |
说明:补充说明信息
注意:
1.这里的ID必须是有效ID,示例的85是我又创建的帖子,现在删除作为演示
成功响应的信息:
json
响应码:200ok```json
响应码:200ok
类型:
{
"code": 0,
"message": "操作成功",
"data": null
}
"data": null } ``` 重点:关键信息
业务逻辑:
1.这里的当前登录的用户ID要是与创建人的ID不一样,此时删除会提示>:
json{ "code": 1006, "message": "不是本人进行改改,修改失败", "data": **null** }2.删除后会处于首页并调用三.5接口的返回值来判断总数量是否减一
3.同时在对应类型的板块下面的帖子数量(articleCount)也减一
五.我的帖子界面
说明:补充说明信息
前言 >:
- 这个界面展示对应用户的基本信息(包括>:头像、发帖数、邮箱、注册日期等)与对应用户发帖的帖子列表(包括>:帖子标题、浏览量、点赞量、回复数)
- 其中若是查询的是当前登录用户 不用传递参数,后端直接从Header读取id ;若是查询的不是当前登录用户 ,则会把对应的用户有效ID传递过去
1.获取用户所有发帖列表详情
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 获取用户所有发帖列表详情 |
| method | get |
| URL | {{base_url}}/article/getAllByUserId? |
| 请求类型 | 无 |
请求信息
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| userId | Long | 否 | 查找的用户的ID | 85 |
响应成功信息
json
响应码>:200ok```json
响应码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": [
{
"id": 86,
"boardId": 1,
"userId": 25,
"title": "你好,9.2",
"visitCount": 7,
"replyCount": 2,
"likeCount": 0,
"state": 0,
"createTime": "2026-09-02 07:24:25",
"updateTime": "2026-09-02 07:26:49",
"own": false
}
]
}
} ```
2.主页个人信息展示
说明:补充说明信息
这个接口和那个一样的,都是需要获取id来访问对应的个人详情来进行前端页面数据的填写
六.个人中心界面
1.获取个人信息
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 获取个人信息 |
| method | get |
| URL | {{base_url}}/user/getUserinfo |
| 请求类型 | 无 |
请求信息
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
成功响应的信息
json
状态码>:200ok```json
状态码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": {
"id": 25,
"username": "202691",
"password": "615bd61d2f7b9f760e217cbb9547a7fb",
"nickname": "202691",
"gender": 2,
"salt": "34ff339ed61f48e199508a0658ed2ebf",
"avatarUrl": null,
"articleCount": 3,
"isAdmin": 0,
"state": 0,
"deleteState": 0,
"createTime": "2026-09-01 11:58:53",
"updateTime": "2026-09-01 11:58:53"
}
}
} ```
2.修改头像
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 修改头像 |
| method | post |
| URL | {{base_url}}/user/modifyAvatar |
| 请求类型 | form-data |
请求信息
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 26 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiJhYjI4OTdjYzVkMmNjOWYwZGRlMGU5NzM0MjgyYzQzOSIsIueUqOaIt-WQjSI6ImRlbGV0ZSIsImV4cCI6MTc5MDI2NzU2Nn0.OMpE5pk3m6wzRrqqK_Z5bf_bRtxinqnR0oclXWkWP4NA-QBDBMCVhFsZ6BAbPPNX |
参数信息
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| avatarFile | MultipartFile | 是 | 修改的用户头像(支持 png、jpeg、jpg、gif) | C:\Users\ASUS\OneDrive\图片\修改头像.png |
成功响应的信息
json
响应码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": {
"userId": 26,
"avatarUrl": "http://localhost:9092/avatars/0be72cdb-178f-4286-9f63-9c8ee6336d7a.png"
}
}
3.修改昵称、邮箱、电话号码
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 修改昵称、邮箱、电话号码 |
| method | post |
| URL | {{base_url}}/user/modifyInfo |
| 请求类型 | application/x-www-form-urlencoded |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 26 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiJhYjI4OTdjYzVkMmNjOWYwZGRlMGU5NzM0MjgyYzQzOSIsIueUqOaIt-WQjSI6ImRlbGV0ZSIsImV4cCI6MTc5MDI2NzU2Nn0.OMpE5pk3m6wzRrqqK_Z5bf_bRtxinqnR0oclXWkWP4NA-QBDBMCVhFsZ6BAbPPNX |
参数信息 >:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| username | String | 否 | 修改的用户名(是个BUG,用户名不应该修改) | 我现在是用户名 |
| nickname | Long | 否 | 修改的用户昵称 | 我现在是用户昵称 |
| gender | Byte | 否 | 修改的性别(0 女生,1男生,2 保密) | 1 |
| String | 否 | 修改的邮箱 | 我现在是邮箱 | |
| phoneNum | String | 否 | 修改的手机号 | 我现在是手机号 |
| remark | String | 否 | 修改的个人简介信息 | 我现在是个人简介信息 |
成功相应的信息>:
json
响应码>:200ok```json
响应码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": {
"id": 26,
"username": "我现在是用户名",
"password": "ab2897cc5d2cc9f0dde0e9734282c439",
"nickname": "我现在是用户昵称",
"phoneNum": "我现在是手机号",
"email": "我现在是邮箱",
"gender": 1,
"salt": "74232457e306445a9e92bc3f7f32dd99",
"avatarUrl": null,
"articleCount": 0,
"isAdmin": 0,
"remark": "我现在是个人简介",
"state": 0,
"deleteState": 0,
"createTime": "2026-09-02 08:55:02",
"updateTime": "2026-09-02 08:55:02"
}
}
} ```
4.修改密码
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 修改密码 |
| method | post |
| URL | {{base_url}}/user/modifyPwd |
| 请求类型 | application/x-www-form-urlencoded |
请求信息>:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息>:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| oldPassword | String | 是 | 原密码 | 123456 |
| newPassword | String | 是 | 新密码 | 654321 |
| passwordRepeat | String | 是 | 二次确认,与新密码一致 | 654321 |
成功响应信息>:
json
响应码>:200ok```json
响应码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": null
}
"data": null } ``` 重点:关键信息
这里修改密码后最好重新登陆来检验密码是否修改成功
七.站内信界面
1.获取站内信信息
警告:注意潜在风险
此接口在三.4处存在
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 获取用户所有的站内信 |
| method | get |
| URL | {{base_url}}/message/getAll |
| 请求类型 | 无 |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
成功响应信息 >:
json
响应码>:200ok```json
响应码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": [
{
"id": 32,
"postUserId": 9,
"receiveUserId": 25,
"content": "你也好呀,这是我给你的回复",
"state": 1,
"createTime": "2026-09-02 01:36:49",
"updateTime": "2026-09-02 01:36:55",
"postUser": {
"id": 9,
"nickname": "test",
"gender": 2,
"avatarUrl": null
}
}
]
}
} ```
2.发送站内信
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 发送站内信 |
| method | post |
| URL | {{base_url}}/message/send |
| 请求类型 | application/x-www-form-urlencoded |
请求信息:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 26 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiJhYjI4OTdjYzVkMmNjOWYwZGRlMGU5NzM0MjgyYzQzOSIsIueUqOaIt-WQjSI6ImRlbGV0ZSIsImV4cCI6MTc5MDI2NzU2Nn0.OMpE5pk3m6wzRrqqK_Z5bf_bRtxinqnR0oclXWkWP4NA-QBDBMCVhFsZ6BAbPPNX |
参数信息:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| receiveUserId | Long | 是 | 站内信的接受者ID | 25 |
| content | String | 是 | 发送的内容 | 这是一个简单的测试 |
成功响应信息:
json
响应码>:200ok```json
响应码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": null
}
"data": null } ```
3.获取站内信的未读数量
警告:注意潜在风险
此接口存在于三.3接口处
4.更新站内信信息为已读
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 更新站内信信息为已读 |
| method | post |
| URL | {{base_url}}/message/markRead |
| 请求类型 | application/x-www-form-urlencoded |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息 >:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| id | Long | 是 | 站内信的ID | 34 |
成功响应信息 >:
json
响应码>:200ok```json
响应码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": null
}
"data": null } ``` 重点:关键信息
注意:
1.站内信息默认为0,0表示未读,1表示已读,2表示已回复
2.此处建议用户发送站内信之后,获取一下站内信列表七.1,在从中拿到对应的id更新状态,后面方便检验
5.站内信回复
接口基本信息
| 参数 | 值 |
|---|---|
| 接口名称 | 站内信回复 |
| method | post |
| URL | {{base_url}}/message/reply |
| 请求类型 | application/x-www-form-urlencoded |
请求信息 >:
| 请求key | 是否必填 | 说明 | 示例 |
|---|---|---|---|
| userId | 是 | 用户登录后存储在local Storage的 userId | 25 |
| user_token | 是 | 用户登陆凭证 | eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiI2MTViZDYxZDJmN2I5Zjc2MGUyMTdjYmI5NTQ3YTdmYiIsIueUqOaIt-WQjSI6IjIwMjY5MSIsImV4cCI6MTc5MDIzODQ2MX0.PUfMPlZq5-xsVAGIg8Qp7vkTP4kvykIV6Vr6uIELh23GFyV922SHR53RU4-PAmEs |
参数信息 >:
| 参数名 | 类型 | 是否必带 | 说明 | 示例 |
|---|---|---|---|---|
| repliedId | Long | 是 | 站内信的ID | 34 |
| content | String | 时 | 回复内容 | 回复 |
成功响应信息>:
jsonjson
响应码>:200ok
类型>:
{
"code": 0,
"message": "操作成功",
"data": null
}
</div>}
提示:实用建议
此接口完了后,业务上是会调用获取站内信的未读数量 与 获取站内信信息 的接口,方便对结果进行检验
2.测试用例的编写
接下来就是根据接口文档开始书写对应的测试用例,包括>:边界值、正常、错误情况、边界值组合、鉴权 等测试用例的编写, 因为时间有限,所以只写了127 个测试用例,像一些场景、弱网测试、sql注入、敏感字段校验等都未涉及到
接口测试用例文档
!CAUTION
1.其中执行状态分为pass通过、failed失败、blocking因为人力资源环境等导致无法进行该测试、NA不具备此功能、NULL用例还未执行默认为NULL,其中除了pass与NULL之外,有问题就在备注里面写原因
2.预计结果统一为JSON格式;动态值(登录返回的userId/user_token、各业务ID、时间等)用<占位符>表示,由前置接口响应动态替换
3.其中TC-22 到TC-24 除了注册与登录接口之外,其余接口都要进行测试
字段类型对照表
| 实体 | 字段 | 类型 |
|---|---|---|
| 用户 user | id / userId | Long |
| 用户 user | username | String |
| 用户 user | password | String |
| 用户 user | nickname | String |
| 用户 user | phoneNum | String(可选) |
| 用户 user | String(可选) | |
| 用户 user | gender | Int |
| 用户 user | salt | String |
| 用户 user | avatarUrl | String/null |
| 用户 user | articleCount | Int |
| 用户 user | isAdmin | Int |
| 用户 user | remark | String(可选) |
| 用户 user | state | Int |
| 用户 user | deleteState | Int |
| 用户 user | createTime / updateTime | String(datetime) |
| 板块 board | id | Long |
| 板块 board | name | String |
| 板块 board | articleCount | Int |
| 板块 board | sort | Int |
| 板块 board | state | Int |
| 板块 board | deleteState | Int |
| 板块 board | createTime / updateTime | String(datetime) |
| 帖子 article | id | Long |
| 帖子 article | boardId | Long |
| 帖子 article | userId | Long |
| 帖子 article | title | String |
| 帖子 article | content | String |
| 帖子 article | visitCount | Int |
| 帖子 article | replyCount | Int |
| 帖子 article | likeCount | Int |
| 帖子 article | state | Int |
| 帖子 article | own | Boolean |
| 帖子 article | createTime / updateTime | String(datetime) |
| 回复 reply | id | Long |
| 回复 reply | articleId | Long |
| 回复 reply | postUserId | Long |
| 回复 reply | content | String |
| 回复 reply | likeCount | Int |
| 回复 reply | state | Int |
| 回复 reply | createTime / updateTime | String(datetime) |
| 站内信 message | id | Long |
| 站内信 message | postUserId | Long |
| 站内信 message | receiveUserId | Long |
| 站内信 message | content | String |
| 站内信 message | state | Int |
| 站内信 message | createTime / updateTime | String(datetime) |
| 登录 login | userId | Long |
| 登录 login | token | String |
| 修改头像 modifyAvatar | userId | Long |
| 修改头像 modifyAvatar | avatarUrl | String |
| 未读数 getUnreadCount | data | Int |
base_url = http://127.0.0.1:9092
接口1:用户注册 {{base_url}}/user/register POST
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-01 | 正常注册 | P0 | username=testuser01 nickname=测试用户01 password=123456 passwordRepeat=123456 | 无 | 发起请求 | {"code":0,"message":"操作成功","data":null} | 首次登录>:{"code":0,"message":"操作成功","data":null} 相同的信息注册>:{"code":1101,"message":"用户已存在","data":null} | pass | 2026.9.3 | zy | testuser01 若存在于库(历史保留数据),那就以全新账号实测注册返回code:0,与接口文档一致 |
| TC-02 | 两次密码不一致 | P0 | username=testuser02 nickname=测试用户02 password=123456 passwordRepeat=654321 | 无 | 发起请求 | {"code":1105,"message":"两次输入的密码不一致","data":null} | {"code":1105,"message":"两次输入的密码不一致","data":null} | pass | 2026.9.3 | zy | |
| TC-03 | username为空 | P0 | username=(传空串) nickname=测试用户03 password=123456 passwordRepeat=123456 | 无 | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":0,"message":"操作成功","data":null} | failed | 2026.9.3 | zy | 服务端未校验username为空,注册返回code:0(预期1002) |
| TC-04 | nickname为空 | P0 | username=testuser04 nickname=(传空串) password=123456 passwordRepeat=123456 | 无 | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":0,"message":"操作成功","data":null} | failed | 2026.9.3 | zy | 服务端未校验nickname为空,注册返回code:0(预期1002) |
| TC-05 | password为空 | P0 | username=testuser05 nickname=测试用户05 password=(传空串) passwordRepeat=(正确的密码) | 无 | 发起请求 | {"code":1105,"message":"两次输入的密码不一致","data":null} | {"code":1105,"message":"两次输入的密码不一致","data":null} | pass | 2026.9.3 | zy | |
| TC-06 | 密码与确认密码均为空 | P0 | username=testuser06 nickname=测试用户06 password=(传空串) passwordRepeat=(传空串) | 无 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 后端未做非空拦截,两项同为空串视为一致直接注册成功 |
| TC-07 | 用户名已存在 | P0 | username=testuser01 nickname=测试用户重复 password=123456 passwordRepeat=123456 | 无 | 发起请求 | {"code":1101,"message":"用户已存在","data":null} | {"code":1101,"message":"用户已存在","data":null} | pass | 2026.9.3 | zy | |
| TC-08 | username超长 | P0 | username=aaaaaaa*45 nickname=测试用户 password=123456 passwordRepeat=123456 | 无 | 发起请求 | {"code":1000,"message":"操作失败","data":null} | { "code": 1000, "message": "会返回很长报错信息,大致意思是这插入的 username 字段内容太长,超过数据库表字段定义的最大长度,MySQL 截断数据抛出异常", "data": null } | failed | 2026.9.3 | zy | 输入45*7字符的username,实际message:"mysql截断数据抛出异常"(预期是""操作失败"") |
| TC-09 | nickname超长 | P0 | username=testuser08 nickname=aaaaaaa*45 password=123456 passwordRepeat=123456 | 无 | 发起请求 | {"code":1000,"message":"操作失败","data":null} | { "code": 1000, "message": "会返回很长报错信息,大致意思是这插入的 nickname 字段内容太长,超过数据库表字段定义的最大长度,MySQL 截断数据抛出异常", "data": null } | failed | 2026.9.3 | zy | 输入45*7字符的nickname,实际message:"mysql截断数据抛出异常"(预期是""操作失败"") |
| TC-10 | password超长 | P0 | username=testuser09 nickname=测试用户09 password=a45字符 passwordRepeat=a45字符 | 无 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | |
| TC-11 | username边界值50字符 | P0 | username=a*20字符 nickname=测试用户 password=123456 passwordRepeat=123456 | 无 | nickname边界值50字符发起请求 | {"code":1000,"message":"校验失败","data":null} | NA | 发起请求2026.9.3 | zy | 未规定username长度限制,后端无此功能,跳过 | |
| TC-12 | nickname边界值50字符 | P0 | username=testuser11 nickname=a*50字符 password=123456 passwordRepeat=123456 | 无 | 发起请求 | {"code":1000,"message":"校验失败","data":null} | NA | 2026.9.3 | zy | 未规定nickname长度限制,后端无此功能,跳过 | |
| TC-13 | password边界值32字符 | P0 | username=testuser12 nickname=测试用户12 password=a32字符 passwordRepeat=a32字符 | 无 | 发起请求 | {"code":1000,"message":"校验失败","data":null} | NA | 2026.9.3 | zy | 未规定password长度限制,后端无此功能,跳过 |
接口2:用户登录 {{base_url}}/user/login POST
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-14 | 正常登录 | P0 | username=testuser01 password=123456 | 无 | 发起请求 | {"code":0,"message":"操作成功","data":{"userId":27,"token":"eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiJjYjg5NTY1NDQ5N2MwYjZiZGMxMGI4NDg0NGJmNWE0MiIsIueUqOaIt-WQjSI6InRlc3R1c2VyMDEiLCJleHAiOjE3OTAzNDA4NTZ9._Mw45V5y-SuRsQ7WjvA_hDDXM9573-yEUfhhtmXyArzQxgjEHx27xcmUj1MtlhU6"}} | {"code":0,"message":"操作成功","data":{"userId":27,"token":"eyJhbGciOiJIUzM4NCJ9.eyLnmbvlvZXlr4bnoIEiOiJjYjg5NTY1NDQ5N2MwYjZiZGMxMGI4NDg0NGJmNWE0MiIsIueUqOaIt-WQjSI6InRlc3R1c2VyMDEiLCJleHAiOjE3OTAzNDA4NTZ9._Mw45V5y-SuRsQ7WjvA_hDDXM9573-yEUfhhtmXyArzQxgjEHx27xcmUj1MtlhU6"}} | pass | 2026.9.3 | zy | |
| TC-15 | 用户名不存在 | P0 | username=notexist password=123456 | 无 | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-16 | 密码错误 | P0 | username=testuser01 password=wrongpwd | 无 | 发起请求 | {"code":1103,"message":"用户名或密码错误","data":null} | {"code":1103,"message":"用户名或密码错误","data":null} | pass | 2026.9.3 | zy | |
| TC-17 | username为空 | P0 | username=(传空串) password=123456 | 无 | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-18 | password为空 | P0 | username=testuser01 password=(传空串) | 无 | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-19 | 两项均为空 | P0 | username=(传空串) password=(传空串) | 无 | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy |
前置依赖:
userId/user_token← 本接口返回data.userId/data.token(后续所有请求头来源)
接口3:主页个人信息展示 {{base_url}}/user/info GET
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| <...> | <...> | <...> | <...> | <...> | <...> | ||||||
| TC-20 | 查询当前用户 | P0 | 请求头:userId+user_token 参数:无id | A用户存在已登录 | 发起请求 | { "code":0, "message":"操作成功", "data":{ "id":userId, "username":"对应用户名", "nickname":"对应用户昵称", "gender":2(默认为2), "avatarUrl":null, "articleCount":0, "isAdmin":0, "state":0, "createTime":"不为空,合法日期时间格式", "updateTime":"不为空,合法日期时间格式" } } | {"code":0,"message":"操作成功","data":{"id":27,"username":"testuser01","nickname":"测试用户01","gender":2,"avatarUrl":null,"articleCount":0,"isAdmin":0,"state":0,"createTime":"2026-09-03 04:49:34","updateTime":"2026-09-03 04:49:34"}} | pass | 2026.9.3 | zy | |
| TC-21 | 查询指定用户 | P0 | 请求头:userId+user_token 参数:id=<userId_B> | 其他用户(如B)注册/存在 | 1.A用户正常登录 2.输入参数开始查找B用户信息 | {"code":0,"message":"操作成功","data":{"id":其他用户id,"username":"他人用户名","nickname":"他人用户昵称","gender":2(默认为2),"avatarUrl":null,"articleCount":0,"isAdmin":0,"state":0,"createTime":"2026-09-03 05:16:34","updateTime":"2026-09-03 05:16:34"}} | {"code":0,"message":"操作成功","data":{"id":28,"username":"otheruser02","nickname":"他人用户02","gender":2,"avatarUrl":null,"articleCount":0,"isAdmin":0,"state":0,"createTime":"2026-09-03 05:16:34","updateTime":"2026-09-03 05:16:34"}} | pass | 2026.9.3 | zy | |
| TC-22 | 查询不存在用户 | P0 | 请求头:userId+user_token 参数:id=999999 | B用户不存在 | 1.A用户正常登录 2.输入参数开始查找B用户信息 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-23 | 未登录访问 | P0 | 请求头:userId无user_token 参数:无 | 用户已注册/存在 | 1.A用户未登录 2.发起请求 | {"code":1100,"message":"用户未登录","data":null} | {"code":1100,"message":"用户未登录","data":null} | pass | 2026.9.3 | zy | |
| TC-24 | token错误 | P0 | 请求头:userId+错误user_token 参数:无 | 用户已注册/存在 | 1.输入错误的token(过期)发起请求 | {"code":1100,"message":"用户未登录","data":null} | {"code":1100,"message":"用户未登录","data":null} | pass | 2026.9.3 | zy | |
| TC-25 | userId与token不匹配 | P0 | 请求头:userId=其他用户+当前user_token 参数:无 | A、B用户已注册/存在 | 1.B用户登录获取token 2.A用户拿到token(B)+userId(A)发起请求 | { "code": 1503, "message": "当前用户无权进行操作", "data": null } | { "code": 1503, "message": "当前用户无权进行操作", "data": null } | pass | 2026.9.3 | zy | |
| TC-26 | 查看自己主页传自己id | P0 | 请求头:userId+user_token(A) 参数:id=<userId_A> | A用户存在已登录 | 传递A用户userId发起请求 | 同上 | {"code":0,"message":"操作成功","data":{"id":27,"username":"testuser01","nickname":"新昵称","gender":2,"avatarUrl":"http://localhost:9092/avatars/b4e03634-767e-4736-8b1e-9f594cf0137b.png","email":"invalid","articleCount":1,"isAdmin":0,"state":0,"remark":"","createTime":"2026-09-03 04:49:34","updateTime":"2026-09-03 05:16:36"}} | pass | 2026.9.3 | zy |
前置依赖:
id(查他人,可选) ← 接口2 登录(账号B) 返回userId;不传则读 Header 当前登录人
接口4:获取导航栏板块信息 {{base_url}}/board/topList GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| <...> | <...> | <...> | <...> | ||||||||
| TC-26 | 正常获取板块 | P0 | 请求头:userId+user_token | A用户存在已登录 | 发起请求 | 有数据时>:{ "code": 0, "message": "操作成功", "data": { "id": 1,(板块ID) "name": "Java",(板块名称) "articleCount": 18,(对应板块下的帖子数) "sort": 1,(排序ID,越小顺序越靠上) "state": 0,(状态: 0 一正常 1 禁用) "deleteState": 0,(是否删除, 0 否, 1 是) "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间" } } 无数据>:{"code":0,"message":"操作成功","data":\[\]} | {"code":0, "message":"操作成功", "data":{"id":1, "name":"Java", "articleCount":6, "sort":1, "state":0, "deleteState":0," createTime":"2023-06-25 02:25:55", "updateTime":"2023-06-25 02:25:55"} | pass | 2026.9.3 | zy |
接口5:获取站内信的未读数量 {{base_url}}/message/getUnreadCount GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| <...> | <...> | <...> | |||||||||
| TC-27 | 正常获取 | P0 | 请求头:userId+user_token | A用户存在已登录 | 发起请求 | {"code":0,"message":"操作成功","data":0}若有未读消息,data则会大于0 | {"code":0,"message":"操作成功","data":0} | pass | 2026.9.3 | zy | |
| TC-28 | 新用户未读数为0 | P0 | 请求头:userId+user_token© | 新用户(如C)注册并登录获取token | 发起请求 | {"code":0,"message":"操作成功","data":0} | {"code":0,"message":"操作成功","data":0} | pass | 2026.9.3 | zy | |
| TC-29 | 收信后未读数+1 | P0 | 请求头:userId+user_token(A) | 1.其他(如C)用户(如A)给当前用户发一封信 2.A用户登录 | 发起请求 | {"code":0,"message":"操作成功","data":1} | {"code":0,"message":"操作成功","data":1} | pass | 2026.9.3 | zy | |
| TC-30 | 标记已读后未读数-1 | P0 | 请求头:userId+user_token(A) | 收到信后调用获取信件接口(触发已读),再获取未读数 | 发起请求 | {"code":0,"message":"操作成功","data":0} | {"code":0,"message":"操作成功","data":0} | pass | 2026.9.3 | zy | TC-29为1时此处为0 |
接口6:获取站内信信息 {{base_url}}/message/getAll GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| <...> | <...> | <...> | |||||||||
| TC-31 | 正常获取 | P0 | 请求头:userId+user_token | A用户存在已登录 | 发起请求 | 无数据时>:{"code":0,"message":"操作成功","data":\[\]} 有数据时>: { "code": 0, "message": "操作成功", "data": { "id": 63,(站内信ID) "postUserId": 93,(发送者ID) "receiveUserId": 25,(接受者ID) "content": "检验\\n",(站内信内容) "state": 0,(状态, 0 耒读, 1 已读, 2 已回复默认 0) "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间", "postUser": { "id": 93,(发送者ID) "nickname": "delete",(发送者昵称) "gender": 2,(性别,0女,1男,2保密) "avatarUrl": null(发送者的头像) } } } 符合格式即可 | {"code":0,"message":"操作成功","data":\[\]} | pass | 2026.9.3 | zy | 无消息时data为\[\] |
| TC-32 | 列表含收到的站内信 | P0 | 请求头:userId+user_token(A) | 1.其他用户(如B)给当前用户(如A)发信 2.当前用户(如A)登录获取token | 发起请求 | 无数据时>:{"code":0,"message":"操作成功","data":\[\]} 有数据时>: { "code": 0, "message": "操作成功", "data": { "id": 63,(站内信ID) "postUserId": 93,(发送者ID) "receiveUserId": 25,(接受者ID) "content": "检验\\n",(站内信内容) "state": 0,(状态, 0 耒读, 1 已读, 2 已回复默认 0) "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间", "postUser": { "id": 93,(发送者ID) "nickname": "delete",(发送者昵称) "gender": 2,(性别,0女,1男,2保密) "avatarUrl": null(发送者的头像) } } } 符合格式即可 | {"code":0,"message":"操作成功","data":{"id":39,"postUserId":27,"receiveUserId":27,"content":"内容","state":0,"createTime":"2026-09-03 05:16:36","updateTime":"2026-09-03 05:16:36","postUser":{"id":27,"nickname":"新昵称","gender":2,"avatarUrl":"http://localhost:9092/avatars/b4e03634-767e-4736-8b1e-9f594cf0137b.png"}},{"id":40,"postUserId":28,"receiveUserId":27,"content":"B发给A的内容","state":2,"createTime":"2026-09-03 05:16:36","updateTime":"2026-09-03 05:16:37","postUser":{"id":28,"nickname":"他人用户02","gender":2,"avatarUrl":null}}} | pass | 2026.9.3 | zy | |
| TC-33 | 标记已读后状态变化 | P0 | 请求头:userId+user_token(A) | 收到站内信后再次调用获取接口,观察状态 | 1.用户(A)收到站内信标记已读(接口24) 2.发送请求 | 同上,对应的已读数量减一 | {"code":0,"message":"操作成功","data":{"id":39,"postUserId":27,"receiveUserId":27,"content":"内容","state":0,"createTime":"2026-09-03 05:16:36","updateTime":"2026-09-03 05:16:36","postUser":{"id":27,"nickname":"新昵称","gender":2,"avatarUrl":"http://localhost:9092/avatars/b4e03634-767e-4736-8b1e-9f594cf0137b.png"}},{"id":40,"postUserId":28,"receiveUserId":27,"content":"B发给A的内容","state":2,"createTime":"2026-09-03 05:16:36","updateTime":"2026-09-03 05:16:37","postUser":{"id":28,"nickname":"他人用户02","gender":2,"avatarUrl":null}}} | pass | 2026.9.3 | zy | 文档:0未读,1已读;其余字段同TC-32 |
| TC-34 | 新用户无站内信 | P0 | 请求头:userId+user_token© | 新用户(如C)注册并登录获取token | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":\[\]} | pass | 2026.9.3 | zy | 文档情况一 |
接口7:根据板块ID查询对应板块下的帖子消息 {{base_url}}/article/getAllByBoardId GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-35 | 正常查询全部帖子 | P0 | 请求头:userId+user_token 参数:无 | 登录获取token | 发起请求 | 有数据时>:{"code":0,"message":"操作成功","data":{"id":XX,"boardId":XX,...}} 无数据>:{"code":0,"message":"操作成功","data":\[\]} | { "code": 0, "message": "操作成功", "data": { "id": 帖子ID, "boardId": 帖子的板块ID, "userId": 帖子作者ID, "title": "帖子的标题", "visitCount": 访问量, "replyCount": 回复数, "likeCount": 点赞数, "state": 帖子状态,默认为0表示正常,1表示封禁, "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间", "own": false, "user": { "id": 帖子作者ID, "nickname": "帖子作者的昵称", "avatarUrl": null(头像地址,默认为空) } } } | pass | 2026.9.3 | zy | |
| TC-36 | 查询指定板块帖子 | P0 | 请求头:userId+user_token 参数:boardId=1(1-9) | 登录获取token | 根据有效board发起请求 | 有数据时>:{"code":0,"message":"操作成功","data":{"id":XX,"boardId":XX,...}} 无数据>:{"code":0,"message":"操作成功","data":\[\]} | { "code": 0, "message": "操作成功", "data": { "id": 帖子ID, "boardId": 帖子的板块ID, "userId": 帖子作者ID, "title": "帖子的标题", "visitCount": 访问量, "replyCount": 回复数, "likeCount": 点赞数, "state": 帖子状态,默认为0表示正常,1表示封禁, "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间", "own": false, "user": { "id": 帖子作者ID, "nickname": "帖子作者的昵称", "avatarUrl": null(头像地址,默认为空) } } } | pass | 2026.9.3 | zy | |
| TC-37 | 板块ID不存在 | P0 | 请求头:userId+user_token 参数:boardId=999或boardId=-1 | 登录获取token | 发起请求 | {"code":0,"message":"操作成功","data":\[\]} | {"code":0,"message":"操作成功","data":\[\]} | pass | 2026.9.3 | zy | |
| TC-38 | 板块ID非法 | P0 | 请求头:userId+user_token 参数:boardId=-1 | 登录获取token | 发起请求 | {"code":1202,"message":"板块不存在","data":null} | {"code":1202,"message":"板块不存在","data":null} | pass | 2026.9.3 | zy |
前置依赖:
boardId← 接口4 topList 返回id
接口8:获取板块的信息 {{base_url}}/board/getById GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-39 | 正常查询 | P0 | 请求头:userId+user_token 参数:id=1 | 登录获取token | 发起请求 | { "code": 0, "message": "操作成功", "data": { "id": 1,(板块ID) "name": "Java",(对应的板块名字) "articleCount": 17,(对应板块下的帖子数量) "sort": 1,(排序优先级,值越小排序时越靠前,最小为1) "state": 0,(板块状态,0<默认>表示正常,1表示禁用) "deleteState": 0,(是否删除,0表示否,1表示是) "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间" } }符合格式即可 | { "code": 0, "message": "操作成功", "data": { "id": 1, "name": "Java", "articleCount": 17, "sort": 1, "state": 0, "deleteState": 0, "createTime": "2023-06-25 02:25:55", "updateTime": "2023-06-25 02:25:55" } } | pass | 2026.9.3 | zy | |
| TC-40 | 板块ID不存在 | P0 | 请求头:userId+user_token 参数:id=999 | 登录获取token | 发起请求 | {"code":1202,"message":"板块不存在","data":null} | {"code":1202,"message":"板块不存在","data":null} | pass | 2026.9.3 | zy | |
| TC-41 | id为空 | P0 | 请求头:userId+user_token 参数:id=(不传参数) | 登录获取token | 发起请求 | {"code":1000,"message":"Required request parameter 'id' for method parameter type Long is not present","data":null} | {"code":1000,"message":"Required request parameter 'id' for method parameter type Long is not present","data":null} | pass | 2026.9.3 | zy | |
| TC-42 | 参数类型错误 | P0 | 请求头:userId+user_token 参数:id=abc123 | 登录获取token | 发起请求 | {"code":1000,"message":,"data":null}这里的message后端会去做对应的类型校验并返回校验信息 | { "code": 1000, "message": "",(返回值是对应的校验信息即可) "data": null } | pass | 2026.9.3 | zy | 建议后端做校验并更新返回状态码 |
前置依赖:
id← 接口4 topList 返回id
接口9:用户进行搜索 {{base_url}}/article/search GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-43 | 正常搜索 | P0 | 请求头:userId+user_token 参数:searchName=test | 登录获取token | 发起请求 | 没有数据时>:{"code":0,"message":"操作成功","data":\[\]} 有数据时>: { "code": 0, "message": "操作成功", "data": { "id": 帖子ID, "boardId": 帖子的板块ID, "userId": 帖子作者ID, "title": "帖子的标题", "visitCount": 访问量, "replyCount": 回复数, "likeCount": 点赞数, "state": 帖子状态,默认为0表示正常,1表示封禁, "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间", "own": false, "user": { "id": 帖子作者ID, "nickname": "帖子作者的昵称", "avatarUrl": null(头像地址,默认为空) } } } | { "code": 0, "message": "操作成功", "data": { "id": 27,(帖子id) "boardId": 3, "userId": 9, "title": "test", "visitCount": 8, "replyCount": 2, "likeCount": 0, "state": 0, "createTime": "2026-03-03 10:45:06", "updateTime": "2026-03-05 09:53:52", "own": false, "user": { "id": 9, "nickname": "test", "avatarUrl": null } } } 满足预期结果的格式都是正确的 | pass | 2026.9.3 | zy | |
| TC-44 | 搜索无结果 | P0 | 请求头:userId+user_token 参数:searchName=不存在关键词 | 登录获取token | 发起请求 | {"code":0,"message":"操作成功","data":\[\]} | {"code":0,"message":"操作成功","data":\[\]} | pass | 2026.9.3 | zy | 文档:搜索无结果显示空列表 |
| TC-45 | searchName为空 | P0 | 请求头:userId+user_token 参数:searchName=(传空串) | 登录获取token | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy |
接口10:用户发送帖子 {{base_url}}/article/create POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-46 | 正常发帖 | P0 | 请求头:userId+user_token 参数:boardId=1,title=测试帖子,content=帖子内容 | 登录获取token | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | |
| TC-47 | boardId不存在 | P0 | 请求头:userId+user_token 参数:boardId=999,title=测试,content=内容 | 登录获取token | 发起请求 | {"code":1202,"message":"板块不存在","data":null} | {"code":1202,"message":"板块不存在","data":null} | pass | 2026.9.3 | zy | |
| TC-48 | title为空 | P0 | 请求头:userId+user_token 参数:boardId=1,title=(传空串),content=内容 | 登录获取token | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-49 | content为空 | P0 | 请求头:userId+user_token 参数:boardId=1,title=标题,content=(传空串) | 登录获取token | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-50 | title超长 | P0 | 请求头:userId+user_token 参数:boardId=1,title=a*101字符,content=内容 | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null} | { "code": 0, "message": "插入数据时,title(文章标题)的内容长度,超过了 MySQL 表中 title 字段设置的最大长度限制", "data": null } |
NA | 2026.9.3 | zy | 后端没有做对应的校验,实际code:0(预期code:1000) |
前置依赖:
boardId← 接口4 topList 返回id(本接口返回 data:null,帖子 id 取自接口7 列表)
接口11:根据帖子ID获取详情信息 {{base_url}}/article/details GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-51 | 正常查询 | P0 | 请求头:userId+user_token 参数:id= | 登录获取token,且该帖子存在 | 发起请求 | { "code": 0, "message": "操作成功", "data": { "id": 帖子ID, "boardId": 帖子的板块ID, "userId": 帖子作者ID, "title": "帖子的标题", "visitCount": 访问量, "replyCount": 回复数, "likeCount": 点赞数, "state": 帖子状态,默认为0表示正常,1表示封禁, "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间", "own": false,(是不是本人,false不是,true是) "user": { "id": 帖子作者ID, "nickname": "帖子作者的昵称", "avatarUrl": null(头像地址,默认为空) }, "board": { "id": 9,(板块ID) "name": "灌水区",(对应板块的标题) "articleCount": 2(对应板块的帖子数) } } } (格式正确即可) | { "code": 0, "message": "操作成功", "data": { "id": 98, "boardId": 9, "userId": 25, "title": "灌水2", "visitCount": 5, "replyCount": 0, "likeCount": 0, "state": 0, "createTime": "2026-09-02 13:57:22", "updateTime": "2026-09-02 13:57:22", "content": "内容2", "own": true, "user": { "id": 25, "nickname": "202691", "avatarUrl": null }, "board": { "id": 9, "name": "灌水区", "articleCount": 2 } } } | pass | 2026.9.3 | zy | 每次调用visitCount+1 |
| TC-52 | 帖子不存在 | P0 | 请求头:userId+user_token 参数:id=999999 | 登录获取token | 发起请求 | {"code":1302,"message":"帖子不存在","data":null} | {"code":1302,"message":"帖子不存在","data":null} | pass | 2026.9.3 | zy | |
| TC-53 | id为空 | P0 | 请求头:userId+user_token 参数:id=(不传参数) | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null}这里的message有提示即可,不强制内容"操作失败" | { "code":1000, "message":"Required request parameter 'id' for method parameter type Long is not present", "data":null } | pass | 2026.9.3 | zy |
前置依赖:
id← 接口7 getAllByBoardId 返回的帖子id
接口12:获取当前帖子的回复列表 {{base_url}}/reply/getReplies GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-54 | 正常查询 | P0 | 请求头:userId+user_token 参数:articleId= | 登录获取token,且存在一篇帖子 | 发起请求 | 没数据时>: {"code":0,"message":"操作成功","data":\[\]} 有数据时>:{ "code": 0, "message": "操作成功", "data": { "id": 79,(评论ID) "articleId": 86,(对应的帖子ID) "postUserId": 9,(对应的评论者ID) "content": "来看我的主页",(对应的评论内容) "likeCount": 0,(点赞数) "state": 0,(状态,0正常,1禁用) "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间", "user": { "id": 9,(对应的评论者ID) "nickname": "test",(对应的评论者昵称) "avatarUrl": null(对应的评论者头像) } } } 实际结果符合格式即可 | { "code": 0, "message": "操作成功", "data": { "id": 79, "articleId": 86, "postUserId": 9, "content": "来看我的主页", "likeCount": 0, "state": 0, "createTime": "2026-09-02 07:26:49", "updateTime": "2026-09-02 07:26:49", "user": { "id": 9, "nickname": "test", "avatarUrl": null } } } | pass | 2026.9.3 | zy | 无回复时data为\[\] |
| TC-55 | 帖子不存在 | P0 | 请求头:userId+user_token 参数:articleId=99999 | 登录获取token | 发起请求 | {"code":1302,"message":"帖子不存在","data":null} | {"code":1302,"message":"帖子不存在","data":null} | pass | 2026.9.3 | zy | 1.articleId过大,会提示"数据长度超过数据库规定字段长度,后端未校验 |
| TC-56 | id为空 | P0 | 请求头:userId+user_token 参数:articleId=(不传参数) | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null} 或者出现"必传参数为传递" | {"code":1000,"message":"Required request parameter 'articleId' for method parameter type Long is not present","data":null} | pass | 2026.9.3 | zy | 建议后端做出校验并更新一下返回状态码 |
前置依赖:
articleId← 接口7 getAllByBoardId 返回的帖子id
接口13:新增帖子回复(评论) {{base_url}}/reply/create POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-57 | 正常回复帖子 | P0 | 请求头:userId+user_token 参数:articleId=,content=回复内容 | 登录获取token,且存在一篇帖子(如A发布) | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 回查接口12:列表出现该回复,帖子replyCount+1 |
| TC-58 | 回复不存在帖子 | P0 | 请求头:userId+user_token 参数:articleId=999999,content=内容 | 登录获取token | 发起请求 | {"code":1302,"message":"帖子不存在","data":null} | {"code":1302,"message":"帖子不存在","data":null} | pass | 2026.9.3 | zy | |
| TC-59 | content为空 | P0 | 请求头:userId+user_token 参数:articleId=,content=(传空串) | 登录获取token | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-60 | content超长 | P0 | 请求头:userId+user_token 参数:articleId=,content=aaaaaaa*501字符 | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null} | {"code":1000,"message":"内容太长,超过数据库字段限制,截断失败","data":null} | failed | 2026.9.3 | zy | 出现数据库层面的错误提示,后端未校验 |
| TC-61 | 未登录回复 | P0 | 请求头:无userId或user_token 参数:articleId=,content=内容 | 无 | 发起请求 | {"code":1100,"message":"用户未登录","data":null} | {"code":1100,"message":"用户未登录","data":null} | pass | 2026.9.3 | zy |
前置依赖:
articleId← 接口7 getAllByBoardId 返回的帖子id
接口14:帖子评论删除 {{base_url}}/reply/deleteReplyed POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-62 | 删除自己的回复 | P0 | 请求头:userId+user_token 参数:articleId=,articleReplyId= | 登录获取token,并在某帖子下发布一条回复 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 回查接口12:该回复已不存在,帖子replyCount-1 |
| TC-63 | 在他人帖子下删除他人回复 | P0 | 请求头:userId+user_token 参数:articleId=<articleId_B>,articleReplyId=<replyId_B> | 其他用户(如B)发布一条回复,当前用户登录获取token | 发起请求 | {"code":1503,"message":"当前用户无权进行操作","data":null} | {"code":1503,"message":"当前用户无权进行操作","data":null} | pass | 2026.9.3 | zy | |
| TC-64 | 在自己的帖子下删除他人回复 | P0 | 请求头:userId+user_token 参数:articleId=,articleReplyId=<replyId_B> | 其他用户(如B)在当前用户帖子下发布一条回复,当前用户登录获取token | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | |
| TC-65 | 回复不存在 | P0 | 请求头:userId+user_token 参数:articleId=,articleReplyId=999999 | 登录获取token | 发起请求 | {"code":1501,"message":"帖子评论不存在","data":null} | {"code":1501,"message":"帖子评论不存在","data":null} | pass | 2026.9.3 | zy | |
| TC-66 | articleReplyId为空 | P0 | 请求头:userId+user_token 参数:articleId=,articleReplyId=(传空串) | 登录获取token | 发起请求 | {"code":1501,"message":"帖子评论不存在","data":null} | {"code":1501,"message":"帖子评论不存在","data":null} | pass | 2026.9.3 | zy |
前置依赖:
articleId← 帖子id;articleReplyId← 接口12 getReplies 返回的回复id
接口15:用户点赞 {{base_url}}/article/thumbsUp POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-67 | 正常点赞 | P0 | 请求头:userId+user_token 参数:id= | 登录获取token,且该帖子存在 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 回查接口11:帖子likeCount+1 |
| TC-68 | 重复点赞 | P0 | 请求头:userId+user_token 参数:id= | 登录获取token,且该帖子存在并已点赞 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | |
| TC-69 | 帖子不存在 | P0 | 请求头:userId+user_token 参数:id=99999 | 登录获取token | 发起请求 | {"code":1302,"message":"帖子不存在","data":null} | {"code":1302,"message":"帖子不存在","data":null} | pass | 2026.9.3 | zy | |
| TC-70 | id为空 | P0 | 请求头:userId+user_token 参数:id=(传空串) | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null}或返回对应的错误信息 | {"code":1000,"message":"Required request parameter 'id' for method parameter type Long is present but converted to null","data":null} | pass | 2026.9.3 | zy | 建议后端做出校验并更新一下返回状态码 |
前置依赖:
id← 接口7 getAllByBoardId 返回的帖子id
接口16:用户修改帖子内容 {{base_url}}/article/modify POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-71 | 正常修改自己帖子 | P0 | 请求头:userId+user_token 参数:id=,title=新标题,content=新内容 | 登录获取token,并发布一篇帖子 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 回查接口11:title/content已更新为新值 |
| TC-72 | 修改他人帖子 | P0 | 请求头:userId+user_token 参数:id=<articleId_B>,title=新标题,content=新内容 | 其他用户(如B)发布一篇帖子,当前用户登录获取token | 发起请求 | {"code":1006,"message":"不是本人进行改改,修改失败","data":null} | {"code":1006,"message":"不是本人进行改改,修改失败","data":null} | pass | 2026.9.3 | zy | 文档明示 |
| TC-73 | 帖子不存在 | P0 | 请求头:userId+user_token 参数:id=999999,title=标题,content=内容 | 登录获取token | 发起请求 | {"code":1302,"message":"帖子不存在","data":null} | {"code":1302,"message":"帖子不存在","data":null} | pass | 2026.9.3 | zy | |
| TC-74 | title为空 | P0 | 请求头:userId+user_token 参数:id=,title=(传空串),content=内容 | 登录获取token,并发布一篇帖子 | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":0,"message":"操作成功","data":null} | failed | 2026.9.3 | zy | 修改帖子title为空时服务端未校验 |
| TC-75 | content为空 | P0 | 请求头:userId+user_token 参数:id=,title=标题,content=(传空串) | 登录获取token,并发布一篇帖子 | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":0,"message":"操作成功","data":null} | failed | 2026.9.3 | zy | 修改帖子content为空时服务端未校验 |
| TC-76 | title超长 | P0 | 请求头:userId+user_token 参数:id=,title=aaaaaaa*101字符,content=内容 | 登录获取token,并发布一篇帖子 | 发起请求 | {"code":1000,"message":"操作失败","data":null} | { "code": 0, "message": "更新的title字段传入的字符串长度,超过 MySQL 表title字段定义的最大长度", "data": null } |
failed | 2026.9.3 | zy | 建议后端做对应的长度校验并更新返回状态码 |
前置依赖:
id← 接口7/接口18 返回的本人帖子id
接口17:用户删除帖子 {{base_url}}/article/delete POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-77 | 删除自己的帖子 | P0 | 请求头:userId+user_token 参数:id= | 登录获取token,并发布一篇帖子 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 回查接口7/18:该帖消失,用户与板块articleCount-1 |
| TC-78 | 删除他人帖子 | P0 | 请求头:userId+user_token 参数:id=<articleId_B> | 其他用户(如B)发布一篇帖子,当前用户登录获取token | 发起请求 | {"code":1006,"message":"不是本人进行改改,修改失败","data":null} | {"code":1006,"message":"不是本人进行改改,修改失败","data":null} | pass | 2026.9.3 | zy | 文档明示 |
| TC-79 | 帖子不存在 | P0 | 请求头:userId+user_token 参数:id=99999 | 登录获取token | 发起请求 | {"code":1302,"message":"帖子不存在","data":null} | {"code":1302,"message":"帖子不存在","data":null} | pass | 2026.9.3 | zy | |
| TC-80 | id为空 | P0 | 请求头:userId+user_token 参数:id=(传空串) | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null}或者有对应的错误信息都可以 | {"code":1000,"message":"Required request parameter 'id' for method parameter type Long is present but converted to null","data":null} | pass | 2026.9.3 | zy |
前置依赖:
id← 接口7/接口18 返回的本人帖子id
接口18:获取用户所有发帖列表详情 {{base_url}}/article/getAllByUserId GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-81 | 查询当前用户 | P0 | 请求头:userId+user_token 参数:无userId | 登录获取token | 发起请求 | 无数据时>: { "code": 0, "message": "操作成功", "data": \[\] } 有数据时>: { "code": 0, "message": "操作成功", "data": { "id": 122,(发帖ID) "boardId": 8,(对应的板块ID) "userId": 25,(对应的发帖人ID) "title": "福利2",(发帖的标题) "visitCount": 0,(浏览数) "replyCount": 0,(评论数) "likeCount": 0,(点赞数) "state": 0,(帖子状态,默认为0表示正常,1表示封禁,) "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间", "own": false } } 格式满足即可 | { "code": 0, "message": "操作成功", "data": { "id": 122, "boardId": 8, "userId": 25, "title": "福利2", "visitCount": 0, "replyCount": 0, "likeCount": 0, "state": 0, "createTime": "2026-09-04 01:57:40", "updateTime": "2026-09-04 01:57:40", "own": false } } | pass | 2026.9.3 | zy | 无帖时data为\[\] 用户发帖列表实际接口为GET /article/getAllByUserId,按实际接口执行通过 |
| TC-82 | 查询指定用户 | P0 | 请求头:userId+user_token 参数:userId=<userId_B> | 其他用户(如B)注册/存在,当前用户登录获取token | 发起请求 | 无数据时>: { "code": 0, "message": "操作成功", "data": \[\] } 有数据时>: { "code": 0, "message": "操作成功", "data": { "id": 122,(发帖ID) "boardId": 8,(对应的板块ID) "userId": 25,(对应的发帖人ID) "title": "福利2",(发帖的标题) "visitCount": 0,(浏览数) "replyCount": 0,(评论数) "likeCount": 0,(点赞数) "state": 0,(帖子状态,默认为0表示正常,1表示封禁,) "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间", "own": false } } 格式满足即可 | { "code": 0, "message": "操作成功", "data": { "id": 123, "boardId": 1, "userId": 93, "title": "这是一个delete测试", "visitCount": 0, "replyCount": 0, "likeCount": 0, "state": 0, "createTime": "2026-09-04 04:27:47", "updateTime": "2026-09-04 04:27:47", "own": false } } | pass | 2026.9.3 | zy | |
| TC-83 | 用户不存在 | P0 | 请求头:userId+user_token 参数:userId=999999 | 登录获取token | 发起请求 | {"code":1102,"message":"用户不存在","data":null} | {"code":1102,"message":"用户不存在","data":null} | pass | 2026.9.3 | zy |
前置依赖:
userId(查他人,可选) ← 接口2 登录(账号B) 返回userId
接口19:获取个人信息 {{base_url}}/user/getUserinfo GET
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-84 | 正常查询 | P0 | 请求头:userId+user_token | 登录获取token | 发起请求 | { "code": 0, // 业务状态码,0代表操作成功 "message": "操作成功", // 返回提示信息 "data": { // 返回业务数据,用户信息实体 "id": 25, // 用户id(当前登录用户ID) "username": "202691", // 登录用户名 "password": "abd0f03b758e1bb66086d8da6e4d18fb", // 加密后的密码(密文,不返回明文) "nickname": "202691", // 用户昵称 "phoneNum": "321", // 手机号码 "gender": 2, // 性别:0女,1男,2未知 "salt": "ba06b7c1f0224345b030c0fb3611e225", // 密码加密盐值 "avatarUrl": null, // 头像地址,null代表未设置头像 "articleCount": 17, // 用户发布帖子总数量 "isAdmin": 0, // 是否管理员:0普通用户,1管理员 "remark": "", // 用户个人简介信息 "state": 0, // 用户账号状态:0正常,1禁用 "deleteState": 0, // 逻辑删除标记:0未删除,1已逻辑删除 "createTime": "格式正确且有效时间", "updateTime": "格式正确且有效时间" } } 格式满足即可 | {"code":0,"message":"操作成功","data":{"id":27,"username":"testuser01","password":"cb895654497c0b6bdc10b84844bf5a42","nickname":"测试用户01","gender":2,"salt":"44896e37616147d68d48786750e6913e","avatarUrl":null,"articleCount":1,"isAdmin":0,"state":0,"deleteState":0,"createTime":"2026-09-03 04:49:34","updateTime":"2026-09-03 04:49:34"}} | pass | 2026.9.3 | zy | 文档响应含password/salt字段 |
接口20:修改头像 {{base_url}}/user/modifyAvatar POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-85 | 正常修改 | P0 | 请求头:userId+user_token 参数:avatarFile=E:\agent\cursor\newFile(new)test_froum_db\Test_forum_db\forum_auto_test\imgs\修改头像.png //用这个地址的头像校验 | 登录获取token | 发起请求 | { "code": 0, "message": "操作成功", "data": { "userId": 25, //当前用户的ID "avatarUrl": "http://localhost:9092/avatars/880670e8-7047-4898-bd33-1ef2e0b11381.png" //修改后的头像地址 } } | {"code":0,"message":"操作成功","data":{"userId":27,"avatarUrl":"http://localhost:9092/avatars/b4e03634-767e-4736-8b1e-9f594cf0137b.png"}} | pass | 2026.9.3 | zy | 回查接口19/3:avatarUrl已更新 |
| TC-86 | 未上传文件 | P0 | 请求头:userId+user_token 参数:avatarFile=不传(未上传文件) | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null} | {"code":1000,"message":"Required request part 'avatarFile' is not present","data":null} | pass | 2026.9.3 | zy | |
| TC-87 | 文件格式错误 | P0 | 请求头:userId+user_token 参数:avatarFile=txt格式文件(非png/jpeg/jpg/gif) | 登录获取token | 发起请求 | {"code":1008,"message":"当前文件格式不正确,仅支持格式:jep、hepg、png、gif、jpg","data":null} | {"code":1008,"message":"当前文件格式不正确,仅支持格式:jep、hepg、png、gif、jpg","data":null} | pass | 2026.9.3 | zy | 文档:仅支持png/jpeg/jpg/gif |
!IMPORTANT
本地仅有一张测试图片
E:\agent\cursor\newFile\(new)test_froum_db\Test_forum_db\forum_auto_test\imgs\修改头像.png,无其他图片资源
接口21:修改昵称、邮箱、电话号码 {{base_url}}/user/modifyInfo POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-88 | 正常修改 | P0 | 请求头:userId+user_token 参数:nickname=新昵称01,gender=1,email=test@qq.com,phoneNum=13800000000,remark=新简介 | 登录获取token | 发起请求 | { "code": 0, "message": "操作成功", "data": { "id": 93, "username": "delete", "password": "8be95f24e7e301d26fe079a6c2e481dd", "nickname": "新昵称01", "phoneNum": "13800000000", "email": "test@qq.com", "gender": 1, "salt": "1204de8402794af899ca0e242f7ba867", "avatarUrl": null, "articleCount": 1, "isAdmin": 0, "remark": "新简介", "state": 0, "deleteState": 0, "createTime": "2026-09-03 14:20:22", "updateTime": "2026-09-03 14:20:22" } } | {"code":0,"message":"操作成功","data":{"id":27,"username":"testuser01","password":"cb895654497c0b6bdc10b84844bf5a42","nickname":"新昵称01","phoneNum":"13800000000","email":"test@qq.com","gender":1,"salt":"44896e37616147d68d48786750e6913e","avatarUrl":"http://localhost:9092/avatars/b4e03634-767e-4736-8b1e-9f594cf0137b.png","articleCount":1,"isAdmin":0,"remark":"新简介","state":0,"deleteState":0,"createTime":"2026-09-03 04:49:34","updateTime":"2026-09-03 05:16:36"}} | pass | 2026.9.3 | zy | 其余字段保持不变 |
| TC-89 | 仅修改昵称 | P0 | 请求头:userId+user_token 参数:nickname=新昵称02 | 登录获取token | 发起请求 | 格式同上,其余字段不变,nickname=新昵称02 | {"code":0,"message":"操作成功","data":{"id":27,"username":"testuser01","password":"cb895654497c0b6bdc10b84844bf5a42","nickname":"新昵称02","phoneNum":"13800000000","email":"test@qq.com","gender":1,"salt":"44896e37616147d68d48786750e6913e","avatarUrl":"http://localhost:9092/avatars/b4e03634-767e-4736-8b1e-9f594cf0137b.png","articleCount":1,"isAdmin":0,"remark":"","state":0,"deleteState":0,"createTime":"2026-09-03 04:49:34","updateTime":"2026-09-03 05:16:36"}} | pass | 2026.9.3 | zy | data其余字段与修改前一致 |
| TC-90 | nickname超长 | P0 | 请求头:userId+user_token 参数:nickname=a*45字符 | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null} | { "code": 1000, "message": "更新用户昵称 nickname,传入的字符串长度超过 MySQL t_user表 nickname 字段设置的最大长度", "data": null } |
failed | 2026.9.3 | zy | 建议后端做校验并更新返回状态码 |
| TC-91 | email格式错误 | P0 | 请求头:userId+user_token 参数:email=invalid | 登录获取token | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":0,"message":"操作成功","data":{"id":27,"username":"testuser01","password":"cb895654497c0b6bdc10b84844bf5a42","nickname":"新昵称","phoneNum":"13800000000","email":"invalid","gender":1,"salt":"44896e37616147d68d48786750e6913e","avatarUrl":"http://localhost:9092/avatars/b4e03634-767e-4736-8b1e-9f594cf0137b.png","articleCount":1,"isAdmin":0,"remark":"","state":0,"deleteState":0,"createTime":"2026-09-03 04:49:34","updateTime":"2026-09-03 05:16:36"}} | failed | 2026.9.3 | zy | email格式错误未校验 |
| TC-92 | gender非法值 | P0 | 请求头:userId+user_token 参数:gender=5 | 登录获取token | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":0,"message":"操作成功","data":{"id":27,"username":"testuser01","password":"cb895654497c0b6bdc10b84844bf5a42","nickname":"新昵称","phoneNum":"13800000000","email":"invalid","gender":2,"salt":"44896e37616147d68d48786750e6913e","avatarUrl":"http://localhost:9092/avatars/b4e03634-767e-4736-8b1e-9f594cf0137b.png","articleCount":1,"isAdmin":0,"remark":"","state":0,"deleteState":0,"createTime":"2026-09-03 04:49:34","updateTime":"2026-09-03 05:16:36"}} | failed | 2026.9.3 | zy | 文档:0女/1男/2保密 gender非法值(5)未校验 |
接口22:修改密码 {{base_url}}/user/modifyPwd POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-93 | 正常修改 | P0 | 请求头:userId+user_token 参数:oldPassword=123456,newPassword=654321,passwordRepeat=654321 | 登录获取token | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 回查接口2:用新密码654321重新登录成功 |
| TC-94 | 旧密码错误 | P0 | 请求头:userId+user_token 参数:oldPassword=wrongpwd,newPassword=654321,passwordRepeat=654321 | 登录获取token | 发起请求 | {"code":1110,"message":"请确定原密码输入正确","data":null} | {"code":1110,"message":"请确定原密码输入正确","data":null} | pass | 2026.9.3 | zy | |
| TC-95 | 新密码两次不一致 | P0 | 请求头:userId+user_token 参数:oldPassword=123456,newPassword=654321,passwordRepeat=111111 | 登录获取token | 发起请求 | {"code":1110,"message":"新密码与二次密码不一致","data":null} | {"code":1110,"message":"新密码与二次密码不一致","data":null} | pass | 2026.9.3 | zy | |
| TC-96 | oldPassword为空 | P0 | 请求头:userId+user_token 参数:oldPassword=(传空串),newPassword=654321,passwordRepeat=654321 | 登录获取token | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-97 | newPassword为空 | P0 | 请求头:userId+user_token 参数:oldPassword=123456,newPassword=(传空串),passwordRepeat=(传空串) | 登录获取token | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-98 | newPassword超长 | P0 | 请求头:userId+user_token 参数:oldPassword=123456,newPassword=a45字符,passwordRepeat=a45字符 | 登录获取token | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy |
!IMPORTANT
这里密码后端是UUID+md5加密/解密,所以超长字符密码可以更新成功
接口23:发送站内信 {{base_url}}/message/send POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-99 | 正常发送 | P0 | 请求头:userId+user_token 参数:receiveUserId=<userId_B>,content=消息内容 | 无 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 回查接口5:接收方未读数+1;接口6:列表新增该条 |
| TC-100 | 发给不存在用户 | P0 | 请求头:userId+user_token 参数:receiveUserId=999999,content=内容 | 无 | 发起请求 | {"code":1102,"message":"用户不存在","data":null} | {"code":0,"message":"操作成功","data":null} | failed | 2026.9.3 | zy | receiveUserId=999999不存在时发送站内信仍返回code0(预期1102) |
| TC-101 | 发给自己 | P0 | 请求头:userId+user_token 参数:receiveUserId=<userId_A>,content=内容 | 无 | 发起请求 | {"code":1406,"message":"站内信不能自己发送给自己","data":null} | {"code":0,"message":"操作成功","data":null} | failed | 2026.9.3 | zy | 接口层面没有做自己给自己发送站内的校验 |
| TC-102 | content为空 | P0 | 请求头:userId+user_token 参数:receiveUserId=<userId_B>,content=(传空串) | 无 | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-103 | content超长 | P0 | 请求头:userId+user_token 参数:receiveUserId=<userId_B>,content=aaaaaaaa*256字符 | 无 | 发起请求 | {"code":1000,"message":"操作失败","data":null} | { "code": 1000, "message": "传入的消息内容content字符串长度,超过t_message表 content 字段定义的最大长度,直接抛出数据库底层异常", "data": null } |
failed | 2026.9.3 | zy | 建议后端添加校验并更新返回状态码 |
| TC-104 | receiveUserId为空 | P0 | 请求头:userId+user_token 参数:receiveUserId=(传空串),content=内容 | 无 | 发起请求 | {"code":1000,"message":"操作失败","data":null} | { "code": 1000, "message": "t_message表receiveUserId接收人ID为非空且无默认值,请求未传入该参数,插入SQL缺失此字段,直接抛出数据库SQL异常。", "data": null } |
failed | 2026.9.3 | zy | 建议后端添加校验并更新返回状态码 |
前置依赖:
receiveUserId← 接口2 登录(账号B) 返回userId
接口24:更新站内信信息为已读 {{base_url}}/message/markRead POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-105 | 正常标记 | P0 | 请求头:userId+user_token 参数:id= | 登录获取token,并收到一条未读站内信 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 回查接口6:该消息state=1;接口5:未读数-1 |
| TC-106 | 消息不存在 | P0 | 请求头:userId+user_token 参数:id=999999 | 登录获取token | 发起请求 | {"code":1402,"message":"站内信不存在","data":null} | {"code":1000,"message":"Cannot invoke ... because message is null","data":null} | failed | 2026.9.3 | zy | 消息不存在时底层抛空指针异常直接返回code:1000(预期1402) |
| TC-107 | 标记他人消息 | P0 | 请求头:userId+user_token 参数:id=<messageId_B> | 用户B收到站内信,当前用户A登录获取token | 发起请求 | {"code":1503,"message":"当前用户无权进行操作","data":null} | {"code":1503,"message":"当前用户无权进行操作","data":null} | pass | 2026.9.3 | zy | |
| TC-108 | id为空 | P0 | 请求头:userId+user_token 参数:id=(传空串) | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null}或出现错误提示 | {"code":1000,"message":"Required request parameter 'id' for method parameter type Long is present but converted to null","data":null} | pass | 2026.9.3 | zy | 建议后端添加校验并更新返回状态码 |
前置依赖:
id← 接口6 getAll 返回的站内信id
接口25:站内信回复 {{base_url}}/message/reply POST
鉴权复用:TC-22~24
| 编号 | 标题 | 优先级 | 测试数据 | 前置条件 | 测试步骤 | 预计结果 | 实际结果 | 执行状态 | 测试时间 | 测试人员 | 备注 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| TC-109 | 正常回复 | P0 | 请求头:userId+user_token 参数:repliedId=,content=回复内容 | 登录获取token,且收到一条站内信 | 发起请求 | {"code":0,"message":"操作成功","data":null} | {"code":0,"message":"操作成功","data":null} | pass | 2026.9.3 | zy | 回查接口6:原消息state=2已回复,对方收到新回复 |
| TC-110 | 消息不存在 | P0 | 请求头:userId+user_token 参数:repliedId=99999,content=内容 | 登录获取token | 发起请求 | {"code":1402,"message":"站内信不存在","data":null} | {"code":1402,"message":"站内信不存在","data":null} | pass | 2026.9.3 | zy | |
| TC-111 | 回复他人消息 | P0 | 请求头:userId+user_token 参数:repliedId=<messageId_B>,content=内容 | 其他用户(如C)给用户B发送一条站内信,当前用户A登录获取token | 发起请求 | {"code":1503,"message":"当前用户无权进行操作","data":null} | {"code":0,"message":"操作成功","data":null} | failed | 2026.9.3 | zy | 越权校验未生效:非接收者A可回复他人(B收到)的站内信 |
| TC-112 | content为空 | P0 | 请求头:userId+user_token 参数:repliedId=,content=(传空串) | 登录获取token | 发起请求 | {"code":1002,"message":"参数校验失败","data":null} | {"code":1002,"message":"参数校验失败","data":null} | pass | 2026.9.3 | zy | |
| TC-113 | content超长 | P0 | 请求头:userId+user_token 参数:repliedId=,content=a*256字符 | 登录获取token | 发起请求 | {"code":1000,"message":"操作失败","data":null} | { "code": 1000, "message": "发送私信时传入超长 content 内容,业务层未做长度校验,直接触发 MysqlDataTruncation 数据库超长异常,对外返回堆栈信息", "data": null } | failed | 2026.9.3 | zy | 建议后端添加校验并更新返回状态码 |
| TC-114 | repliedId为空 | P0 | 请求头:userId+user_token 参数:repliedId=(传空串),content=内容 | 登录获取token | 发起请求 | {"code":1402,"message":"站内信不存在","data":null} | {"code":1402,"message":"站内信不存在","data":null} | pass | 2026.9.3 | zy |
前置依赖:
repliedId← 接口6 getAll 返回的站内信id
3.编写自动化测试脚本
1>.先制定目录
json
forum_auto_test/
├── pytest.ini # pytest 配置
├── requirements.txt # pytest/requests/pyyaml/allure-pytest/jsonschema
├── conftest.py # 全局 fixture:账号初始化、变量池、依赖注入
│
├── config/
│ ├── settings.py # base_url、账号、超时、日志路径等常量
│ └── env.yaml # 环境配置(base_url、账号A/B/C、头像路径)
│
├── common/
│ ├── base_client.py # requests 封装:统一 headers、异常处理、日志
│ ├── context.py # session 级变量池(存动态返回值)
│ ├── extractor.py # 依赖解析:<变量> 替换、响应字段提取回写
│ ├── yaml_loader.py # YAML 数据加载
│ ├── logger.py # logging:分级 + 按天分割
│ └── assertion.py # jsonschema 断言 + code/message/data 校验
│
├── pages/ # POM 核心:接口对象层(按业务模块封装)
│ ├── base_page.py # 页面基类:封装公共 send/assert 能力
│ ├── user_page.py # 注册/登录/个人信息/头像/昵称/密码
│ ├── board_page.py # 板块:topList/getById
│ ├── article_page.py # 帖子:发帖/详情/搜索/点赞/改/删/列表
│ ├── reply_page.py # 回复:列表/新增/删除
│ └── message_page.py # 站内信:列表/未读数/发送/已读/回复
│
├── data/
│ ├── testcases/
│ │ ├── user.yaml # 用户模块用例数据
│ │ ├── board.yaml # 板块模块用例数据
│ │ ├── article.yaml # 帖子模块用例数据
│ │ ├── reply.yaml # 回复模块用例数据
│ │ └── message.yaml # 站内信模块用例数据
│ └── schemas/
│ ├── user_schema.json
│ ├── board_schema.json
│ ├── article_schema.json
│ ├── reply_schema.json
│ └── message_schema.json
│
├── testcases/ # 测试层,调用 pages 对象方法
│ ├── test_user.py
│ ├── test_board.py
│ ├── test_article.py
│ ├── test_reply.py
│ └── test_message.py
│
├── logs/ # 按级别(error与info)后在按照天分割日志
| |---->error
| |----->2026.9.10_error.log
| |----->2026.9.11_error.log
| |---->info
| |----->2026.9.10_info.log
| |----->2026.9.11_info.log
|
|
|
└── reports/
└── allure-results/ # allure 结果
|------>run_result # 存储运行结果,比如>:json、txt
|------>run_report # 最终的allure生成的静态html
2>.搭建框架与生成脚本
代码放在仓库,需要可以自行查看>:
forum_auto_test · 黄昏恋慕黎明/博客论坛项目测试迭代 - 码云 - 开源中国
3>.prompt
如今已经进入 AI 时代,借助 AI 赋能研发已经成为提升工作效率的重要手段。对我们工程师而言,AI 赋能不等于直接把问题丢给大模型,核心公式是:项目业务理解 + 高质量提示词 + 源码与问题分析能力
此处就演示一下我的提示词(小白还在学习中)>:
bash
一.一开始让AI修改接口文档的提示词
背景:我正在做接口自动化测试,现有一份基础接口文档,文档不定义失败响应格式,异常响应交由后续测试用例单独覆盖。
任务:对这份接口文档做优化。
约束要求:
1. 语言精简清晰,去除冗余信息。
2. 禁止修改原有参数表格(表格内示例值保留不动,示例会随项目迭代失效,仅作参考)
3.如果某个入参来源于其他接口的返回字段,在该参数表格下方追加标注【如何拿到它】,写明来源接口与对应返回字段
4. 梳理接口之间的关联关系:包含接口执行先后顺序、接口返回字段的校验断言关系
5. 为了方便你去优化,我给你提供了对应的sql语句在对应的文档下面
5. 输出聚焦核心信息,不做多余扩展。
输出>:博客接口文档.md
输出>:在原有的基础上修改
二.让AI生成测试用例的提示词
背景:已有固定测试用例模板,参考【博客接口文档.md】的接口说明向模版【博客论坛测试用例.md】完善
任务:基于博客接口文档生成接口测试用例
要求:
1.严格使用给定模板,不得修改模板字段与格式,禁止额外多余文字
2.用例必须覆盖:正向(正常、合法失败、有效参数组合)、逆向(边界值、必填不传/传空、异常选填参数)、安全(鉴权、越权访问修改他人帖子数据)
3.每个接口最多12条用例,全部以表格输出
输入:博客接口文档.md、博客论坛测试用例.md
输出:博客论坛测试用例.md,仅输出测试用例表格,不要思考解释
三.自己检查测试用例是否合理,并去postman执行测试用例来填写预期结果实际结果等字段
四.让AI生成对应的目录
要求>:根据接⼝测试⽤例.md⽂件以及接口文档内容,直接输出接⼝⾃动化⽬录结构即可,不需要创建项⽬代码或⽂件
技术栈要求:
1. 编程语⾔:Python
2. 测试框架:pytest
3. HTTP 库:requests
4. 数据驱动:YAML
5. 报告:Allure
6. 接⼝返回值断⾔:jsonschema
7. logging⽇志记录:⽇志分级输出,按天分割
其他设计要求:比如有些接口的参数需要依赖其他的的返回值,也就是动态获取,这些依赖关系全在接口文档里对应接口下有提到
五.开始写代码
背景:接口文档、完整测试用例、项目目录结构均已准备就绪。
任务:基于现有接口文档与测试用例,自动生成对应的自动化测试代码。
要求:
1. 严格遵守项目已约定好的目录格式规范,禁止自行创建多余文件、新增未约定的目录,所有代码输出放到已有对应模块下。
2. 编码必须尊重测试用例中写明的<接口业务依赖关系>:存在前置依赖接口时,执行顺序、数据传递(如 user_token、业务 id、用户userId时)需要处理,不能无视用例依赖直接独立执行单条接口。
3. 注重代码复用性:抽取公共方法、公共变量,避免重复冗余代码;不生成无业务意义、无效的样板垃圾代码
4. 第一步只输出测试计划,不生成任何业务测试代码;等待我审阅确认测试计划无误之后,再进行后续编码、代码补充与优化工作
六.后续就是运行AI创建的代码,结合日志去修复代码,比如>:jsonschema断言的使用、有无动态获取ID信息等
4.问题
在使用AI时,我们不能无脑去依赖AI,应该在每一步AI后人工去检查是否合理,比如在让AI生成目标与代码就出现以下问题>:
(1).AI根据项目框架生成的脚本目录出现缺失 、错误的情况,且文件夹出现.py后缀
比如>: logs日志没有按照级和天数分割,schema包下的reply_list_schema.json与article_detail_schema.json均未生成
(2).requests 库没有下载,且下载对应的库存在问题,pytest与allure-pytest库版本过高 导致无法正确收集测试结果到指定目录,会导致生成的静态报告不准确
(3).测试用例数量不够,且存在一些配置或者共有的方法没有被调用
(4).测试脚本存在jsonschema校验缺失 ,且存在测试文件没有使用参数化
(5).生成代码后部分文件无代码并且像user_info_schema.json和settings.yaml 代码复用率底
(6).AI幻觉,AI解决问题后问题未解决且错误提示出现了架构没有的包名称
5.测试报告


可看到运行了127测试用例,pass 111,failed 12个且都符合预期,4个NA
6.缺陷以及优化
这些测试失败可以分为三个点,即基础参数校验缺失 、 长度溢出与底层异常处理不当 、业务逻辑漏洞
1>. 基础参数校验缺失(空值、格式与非法枚举)
现象描述 :
在多个接口中未能对****必填字段的非空 (空字符串)、格式 以及枚举值 进行有效拦截,导致脏数据直接进入数据库或产生错误的业务结果。
测试表现为:预期抛出参数校验失败 的错误码,但实际后端返回了 code: 0 (操作成功)
涉及用例 (共 6 个):
- TC-03 :注册时
username为空串,实际返回 0(预期 1002)。 - TC-04 :注册时
nickname为空串,实际返回 0(预期 1002)。 - TC-74 :修改帖子时
title为空串,实际返回 0(预期 1002)。 - TC-75 :修改帖子时
content为空串,实际返回 0(预期 1002)。 - TC-91 :修改个人信息时,
email格式非法(如传入 "invalid"),实际返回 0 且未拦截(预期 1002)。 - TC-92 :修改个人信息时,
gender传入非法值(如 5,超出了枚举 0/1/2 的范围),实际返回 0(预期 1002)。
优化分析与建议:
1.提前拦截>:在Controller层对空值、长度进行校验拦截
2.业务拦截>:在业务层对一些关键信息的加上非空以及格式的二次校验
比如>:
a. 用户注册在password与passewordRepeat都为空的情况没有做判断,导致出现脏数据
b. 用户修改邮箱格式没有对后缀进行校验.导致非法字符可以注册成功
2>.长度溢出与底层异常处理不当
现象描述:
系统没有对一些超长参数字段未做长度限制检查,后端也未做对应的响应数据返回,导致数据库成面报错
涉及用例 (共 3 个):
- TC-09 :注册时
nickname超长(超过数据库字段限制),预期应该返回失败FAILED(1000 ,"操作失败"),但系统响应码是code:0 - TC-90 :修改个人信息时
nickname超长,表现与 TC-09 一致(预期 1000,但实际是code: 0)。 - TC-106 :调用标记站内信已读接口时,若传入一个不存在的站内信ID(messageId), 系统底层直接抛出了空指针异常 ,返回了操作失败(code: 1000) ,预期应返回 1402 站内信不存在
优化分析与建议:
-
控制输入长度 :在 Controller 层引入对应的Validated注解提前校验拦截
java比如>: @Size(max = 50, message = "长度不能超过50字符") @Length(min = 1,message = "文章标题长度至少输入一位") -
异常处理优化:针对于一些异常,比如超长参数的response做对应的异常拦截,配合异常处理返回**{"code": 1002, "message": "参数校验失败"}**
3>. 业务逻辑漏洞与边界判定缺失
现象描述 :
部分核心业务处理时缺乏必要的前提条件判断 和数据所有权验证 ,导致出现了不合逻辑 的操作以及高危的越权风险
涉及用例 (共 3 个):
- TC-100 :给一个不存在的用户(如
receiveUserId=999999)发送站内信。预期应该报错用户不存在(1102),但实际后端未做存在校验,直接返回了code: 0。 - TC-101 :自己给自己发送站内信。预期应该受到业务拦截(1406 不能给自己发信),但实际畅通无阻返回
code: 0。 - TC-111(越权) 用户 A 能够通过
/message/reply接口,回复 一条用户 C 发给用户 B 的站内信。系统未校验回复者是否为信件的接收方,预期应该返回1503 当前用户无权进行操作,实际却返回code: 0操作成功。
优化分析与建议:
- 数据前置查询与校验 :在服务层(Service)处理前必须查库验证资源存在性,例如发信前必须校验
receiveUser存在(否则抛 1102) - 数据所属权校验:在对私有数据进行操作时(尤其是站内信回复),必须验证当前操作的用户 ID是否是对应站内信(messageId)里对应的字段接受者Id(receivedId)
二.WebUI测试
之前我们是java写的脚本,运行时也是主动创建类 在方法里面实例化 对象,并调用方法挨个运行的,因为我们项目小,所以可以这样操作,但是当项目大了之后,非常不利于我们管理和运行测试用例,此处我们迭代并引入pytest框架重构下代码
1.自动化框架
forum_db_selenium/ # 论坛自动化测试项目根目录
│
├── 配置文件
│ ├── .gitignore # Git 忽略文件配置
│ ├── pytest.ini # Pytest 配置(测试发现规则、Allure 集成、日志置)
│ ├── conftest.py # Pytest 全局fixture与HOOK(WebDriver管理、失败截图)
│ └── requirements.txt # Python 依赖清单
│
├── Pytest 测试层(32 个测试用例)
│ └── tests/ # 测试用例目录
│ ├── __init__.py
│ ├── test_register.py # @pytest.mark.order(1) - 注册测试(4 个用例)
│ ├── test_login.py # @pytest.mark.order(2) - 登录测试(6 个用例)
│ ├── test_home.py # @pytest.mark.order(3) - 首页测试(2 个用例)
│ ├── test_publish.py # @pytest.mark.order(4) - 发布帖子测试(5 个用例)
│ ├── test_details.py # @pytest.mark.order(5) - 帖子详情测试(6 个用例)
│ ├── test_personal_center.py # @pytest.mark.order(6) - 个人中心测试(3 个用例)
│ ├── test_my_post.py # @pytest.mark.order(7) - 我的帖子测试(2 个用例)
│ └── test_message.py # @pytest.mark.order(8) - 站内信测试(4 个用例)
|--------> conftest.py # 全局的fixture配置,包括截图工具、driver及对应的配置
|-------->pytest.ini # 测试目录、命令行参数、#日志配置、标记定义等
├── logs/ # 按级别(error与info)后在按照天分割日志
| |---->error
| |----->2026.9.10_error.log
| |----->2026.9.11_error.log
| |---->info
| |----->2026.9.10_info.log
| |----->2026.9.11_info.log
├── test_resources # 测试资源
├── reports/ # 测试报告目录
| |-------> allure_result # 存放测试结果(json、txt文件等)
| |-------> allure_report # 测试报告
│
└── src/
└── img/ # 测试截图存储目录
└── 2026-09-08/ # 按日期分组的截图
2.自动化代码
1>.全局fixture
python
# -*- coding: utf-8 -*-
"""
Pytest 全局配置文件
提供 fixture 和 hook 函数
"""
import os
import time
from datetime import datetime
import allure
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
from Util.util import Util
@pytest.fixture(scope="session")
def driver():
"""
WebDriver 会话级 fixture
所有测试共享同一个浏览器实例,与原 Util.webDriver 静态字段保持一致
"""
if Util.webDriver is None:
options = Options()
options.add_argument("--remote-allow-origins=*")
options.add_experimental_option("detach", True)
Util.webDriver = webdriver.Chrome(options=options)
Util.webDriver.implicitly_wait(3)
Util.webDriver.maximize_window()
Util.flag = False
yield Util.webDriver
# 会话结束后关闭浏览器(可选)
# Util.webDriver.quit()
@pytest.fixture(scope="function")
def screenshot_path():
"""生成截图路径"""
new_year = datetime.now().strftime("%Y-%m-%d")
new_hour = datetime.now().strftime("%M-%H-%S")
base_dir = f"./src/img/{new_year}/"
os.makedirs(base_dir, exist_ok=True)
return base_dir, new_hour
@pytest.hookimpl(hookwrapper=True, tryfirst=True)
def pytest_runtest_makereport(item, call):
"""
测试执行报告钩子
失败时自动截图并附加到 Allure 报告
"""
outcome = yield
report = outcome.get_result()
if report.when == "call":
# 添加测试结果到 Allure
if report.passed:
allure.dynamic.tag("PASSED")
elif report.failed:
allure.dynamic.tag("FAILED")
# 失败时截图
if Util.webDriver is not None:
try:
new_year = datetime.now().strftime("%Y-%m-%d")
new_hour = datetime.now().strftime("%M-%H-%S")
test_name = item.nodeid.replace("::", "-").replace("/", "-")
file_name = f"./src/img/{new_year}/FAILED-{test_name}-{new_hour}.png"
os.makedirs(os.path.dirname(file_name), exist_ok=True)
Util.webDriver.save_screenshot(file_name)
# 附加到 Allure 报告
with open(file_name, "rb") as f:
allure.attach(
f.read(),
name=f"失败截图-{test_name}",
attachment_type=allure.attachment_type.PNG
)
except Exception as e:
print(f"截图失败: {e}")
elif report.skipped:
allure.dynamic.tag("SKIPPED")
def pytest_configure(config):
"""
Pytest 配置钩子
设置 Allure 环境信息
"""
# 创建 allure-results 目录
allure_dir = config.getoption("--alluredir")
if allure_dir and not os.path.exists(allure_dir):
os.makedirs(allure_dir)
# 写入环境信息
if allure_dir:
env_properties = os.path.join(allure_dir, "environment.properties")
with open(env_properties, "w", encoding="utf-8") as f:
f.write("Browser=Chrome\n")
f.write("Base.URL=http://127.0.0.1:9092\n")
f.write("Python.Version=3.14.7\n")
f.write("Selenium.Version=4.21.0+\n")
f.write(f"Test.Date={datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
@pytest.fixture(scope="function")
def login_user(driver):
"""
登录 fixture
可选:在需要登录状态的测试中使用
"""
def _login(username="testName_001", password="123456"):
Util.login(username, password)
return _login
@pytest.fixture(scope="function", autouse=False)
def clear_login_state(driver):
"""
清除登录状态 fixture
在需要未登录状态的测试前使用
"""
driver.execute_script("window.localStorage.clear();")
driver.refresh()
time.sleep(0.5)
@pytest.fixture
def screenshot_helper(driver):
"""
截图辅助 fixture
不导航页面,直接截图并附加到 Allure 报告
用于替代 Util(url) 实例创建导致的页面刷新问题
"""
def take_screenshot(name):
timestamp = datetime.now().strftime("%Y-%m-%d")
time_str = datetime.now().strftime("%M-%H-%S")
path = f"./src/img/{timestamp}/{name}-{time_str}.png"
os.makedirs(os.path.dirname(path), exist_ok=True)
driver.save_screenshot(path)
# 附加到 Allure 报告
with open(path, "rb") as f:
allure.attach(
f.read(),
name=name,
attachment_type=allure.attachment_type.PNG
)
return path
return take_screenshot
@pytest.fixture(scope="function")
def slide_to_element(driver):
"""返回将页面滚动至指定元素的函数。"""
def scroll_to_element(selector):
target = driver.find_element(By.CSS_SELECTOR, selector)
driver.execute_script(
"arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'})", target)
time.sleep(0.5)
return target
return scroll_to_element
2>.pytest.ini配置
python
[pytest]
# 测试文件发现规则
python_files = test_*.py
python_classes = Test*
python_functions = test_*
# 测试目录
testpaths = tests
# 命令行参数
addopts =
-v
--tb=short
--strict-markers
--alluredir="reports\allure-results\run_result"
--clean-alluredir
# 日志配置
log_cli = true
log_cli_level = INFO
log_cli_format = %(asctime)s [%(levelname)s] %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S
# 标记定义
markers =
order: 指定测试执行顺序 # 控制测试按 1-8 的顺序运行
smoke: 冒烟测试 # 核心功能快速验证
regression: 回归测试 # 完整功能回归
login: 登录相关测试 # 只运行登录相关
register: 注册相关测试 # 只运行注册相关
publish: 发布帖子相关测试 # 只运行发布功能
message: 站内信相关测试 # 只运行站内信功能
3>注册页面 test_register
python
import time
import allure
import pytest
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from register.register import Register
@pytest.fixture(scope="class")
def register_page():
"""类级别 fixture,共享 Register 实例"""
register = Register()
yield register
@pytest.mark.order(1)
@allure.feature("用户注册")
class TestRegister:
"""用户注册测试类"""
@allure.story("注册页面检查")
@allure.title("验证注册页面正常打开")
def test_register_page_right(self, register_page):
"""注册页面正常打开"""
with allure.step("检查 Logo 元素"):
register_page.webDriver.find_element(By.CSS_SELECTOR,
"body > div > div > div > div:nth-child(1) > div > div.text-center.mb-4 > img")
with allure.step("检查注册按钮"):
register_page.webDriver.find_element(By.CSS_SELECTOR,
"body > div > div > div > div:nth-child(1) > div > div.text-center.text-muted.mt-3 > a")
print("页面正常打开")
allure.attach("页面正常打开", name="测试结果", attachment_type=allure.attachment_type.TEXT)
@allure.story("注册失败-用户名已存在")
@allure.title("用户名 {username} 已存在,注册失败")
@pytest.mark.parametrize("username,nickname,password", [
("zhangsan", "zhangsan", "123456")
])
def test_register_page_failed_one(self, register_page, username, nickname, password):
"""注册错误 1(用户名存在)"""
with allure.step("点击注册按钮进入注册页面"):
register_page.webDriver.find_element(By.CSS_SELECTOR,
"body > div > div > div > div:nth-child(1) > div > div.text-center.text-muted.mt-3 > a").click()
with allure.step(f"输入用户名: {username}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#username").send_keys(username)
with allure.step(f"输入昵称: {nickname}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#nickname").send_keys(nickname)
with allure.step(f"输入密码: {password}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#password").send_keys(password)
with allure.step("输入确认密码"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#passwordRepeat").send_keys(password)
with allure.step("勾选同意使用条款和隐私服务"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#policy").click()
with allure.step("点击注册提交"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#submit").click()
with allure.step("验证错误提示"):
fail01 = register_page.webDriver.find_element(By.CSS_SELECTOR, "body > div.jq-toast-wrap.bottom-right > div > h2").text
time.sleep(0.5)
register_page.get_screen_shot(type(register_page).__name__ + "_test_register_page_failed_one")
print(fail01)
assert fail01 == "warning", f"期望提示为 'warning',实际为 '{fail01}'"
allure.attach(f"错误提示: {fail01}", name="验证结果", attachment_type=allure.attachment_type.TEXT)
@allure.story("注册失败-密码和确认密码不一致")
@allure.title("密码和确认密码不一致,注册失败")
@pytest.mark.parametrize("username,nickname,password,password_repeat,expected", [
("zhangsan", "zhangsan", "123456", "123", "请检查确认密码")
])
def test_register_fail_two(self, register_page, username, nickname, password, password_repeat, expected):
"""密码和确认密码不一致"""
with allure.step("刷新页面清空表单"):
register_page.webDriver.refresh()
with allure.step(f"输入用户名: {username}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#username").send_keys(username)
with allure.step(f"输入昵称: {nickname}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#nickname").send_keys(nickname)
with allure.step(f"输入密码: {password}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#password").send_keys(password)
with allure.step(f"输入确认密码: {password_repeat}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#passwordRepeat").send_keys(password_repeat)
with allure.step("勾选同意隐私"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#policy").click()
with allure.step("点击注册"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#submit").click()
with allure.step("验证错误提示"):
fail02 = register_page.webDriver.find_element(By.CSS_SELECTOR,
"#signUpForm > div > div:nth-child(5) > div > div").text
time.sleep(0.5)
register_page.get_screen_shot(type(register_page).__name__ + "_test_register_fail_two")
assert fail02 == expected, f"期望提示为 '{expected}',实际为 '{fail02}'"
allure.attach(f"错误提示: {fail02}", name="验证结果", attachment_type=allure.attachment_type.TEXT)
@allure.story("注册成功")
@allure.title("用户 {username} 注册成功")
@pytest.mark.parametrize("username,nickname,password", [
("testName_001", "testName_001", "123456")
])
def test_register_suc(self, register_page, username, nickname, password):
"""注册成功"""
with allure.step("刷新页面清空表单"):
register_page.webDriver.refresh()
with allure.step(f"输入用户名: {username}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#username").send_keys(username)
with allure.step(f"输入昵称: {nickname}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#nickname").send_keys(nickname)
with allure.step(f"输入密码: {password}"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#password").send_keys(password)
with allure.step("输入确认密码"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#passwordRepeat").send_keys(password)
with allure.step("勾选同意条款"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#policy").click()
with allure.step("点击注册"):
register_page.webDriver.find_element(By.CSS_SELECTOR, "#submit").click()
with allure.step("验证注册成功,跳转到登录页面"):
wait = WebDriverWait(register_page.webDriver, 3)
wait.until(EC.presence_of_element_located(
(By.CSS_SELECTOR, "body > div > div > div > div:nth-child(1) > div > div.card.card-md > div > h2")))
register_page.get_screen_shot(type(register_page).__name__ + "_test_register_suc")
ele = register_page.webDriver.current_url
expected_url = "http://127.0.0.1:9092/sign-in.html"
assert ele == expected_url, f"期望跳转到 '{expected_url}',实际为 '{ele}'"
allure.attach(f"注册成功,跳转到: {ele}", name="验证结果", attachment_type=allure.attachment_type.TEXT)
4>.登录页面 test_login.py
python
import time
import pytest
import allure
from selenium.webdriver.common.by import By
from Util.util import Util
@pytest.mark.order(2)
@allure.feature("用户登录")
class TestLogin:
URL = "http://127.0.0.1:9092/sign-in.html"
@pytest.fixture(autouse=True)
def setup(self, driver):
"""每个测试前导航到登录页"""
self.webDriver = driver
Util.webDriver = driver
self.webDriver.get(self.URL)
@allure.story("登录页面检查")
@allure.title("检查登录页面元素正常显示")
def test_register_page_right(self):
"""检查页面是否可以正常打开"""
with allure.step("检查论坛logo图标"):
self.webDriver.find_element(By.CSS_SELECTOR,
"body > div > div > div > div:nth-child(1) > div > div.text-center.mb-4 > img")
with allure.step("检查注册按钮"):
self.webDriver.find_element(By.CSS_SELECTOR,
"body > div > div > div > div:nth-child(1) > div > div.text-center.text-muted.mt-3 > a")
print("页面正常打开")
@allure.story("登录失败-密码错误")
@allure.title("密码错误时登录失败")
@pytest.mark.parametrize("username,password,expected_msg", [
("testName_001", "123", "警告")
])
def test_login_fail01(self, username, password, expected_msg, screenshot_helper):
"""检查登录失败01--密码错误"""
with allure.step(f"输入用户名 {username}"):
self.webDriver.find_element(By.CSS_SELECTOR, "#username").send_keys(username)
with allure.step(f"输入错误密码 {password}"):
self.webDriver.find_element(By.CSS_SELECTOR, "#password").send_keys(password)
with allure.step("点击登录按钮"):
self.webDriver.find_element(By.CSS_SELECTOR, "#submit").click()
with allure.step("验证显示警告信息"):
fail01 = self.webDriver.find_element(By.CSS_SELECTOR, "body > div.jq-toast-wrap.bottom-right > div > h2").text
time.sleep(0.5)
screenshot_helper(f"test_login_fail01")
assert fail01 == expected_msg, f"期望 '{expected_msg}',实际 '{fail01}'"
@allure.story("登录失败-密码为空")
@allure.title("密码为空时登录失败")
def test_login_fail02(self, screenshot_helper):
"""密码为空"""
with allure.step("刷新页面"):
self.webDriver.refresh()
with allure.step("输入用户名 testName_001"):
self.webDriver.find_element(By.CSS_SELECTOR, "#username").send_keys("testName_001")
with allure.step("不输入密码"):
self.webDriver.find_element(By.CSS_SELECTOR, "#password")
with allure.step("点击登录按钮"):
self.webDriver.find_element(By.CSS_SELECTOR, "#submit").click()
with allure.step("验证显示'密码不能为空'提示"):
fail03 = self.webDriver.find_element(By.CSS_SELECTOR, "#signInForm > div.mb-2 > div > div").text
screenshot_helper(f"test_login_fail02")
assert fail03 == "密码不能为空", f"期望 '密码不能为空',实际 '{fail03}'"
@allure.story("登录失败-用户名为空")
@allure.title("用户名为空时登录失败")
def test_login_fail03(self, screenshot_helper):
"""用户名为空"""
with allure.step("刷新页面"):
self.webDriver.refresh()
with allure.step("不输入用户名"):
self.webDriver.find_element(By.CSS_SELECTOR, "#username")
with allure.step("输入密码 111111"):
self.webDriver.find_element(By.CSS_SELECTOR, "#password").send_keys("111111")
with allure.step("点击登录按钮"):
self.webDriver.find_element(By.CSS_SELECTOR, "#submit").click()
with allure.step("验证显示'用户名不能为空'提示"):
fail02 = self.webDriver.find_element(By.CSS_SELECTOR, "#signInForm > div.mb-3 > div").text
screenshot_helper(f"test_login_fail03")
assert fail02 == "用户名不能为空", f"期望 '用户名不能为空',实际 '{fail02}'"
@allure.story("登录失败-用户名错误")
@allure.title("用户名错误时登录失败")
@pytest.mark.parametrize("username,password,expected_msg", [
("testNa_00", "123456", "警告")
])
def test_login_failed04(self, username, password, expected_msg, screenshot_helper):
"""用户名错误"""
with allure.step("刷新页面"):
self.webDriver.refresh()
with allure.step(f"输入错误用户名 {username}"):
self.webDriver.find_element(By.CSS_SELECTOR, "#username").send_keys(username)
with allure.step(f"输入密码 {password}"):
self.webDriver.find_element(By.CSS_SELECTOR, "#password").send_keys(password)
with allure.step("点击登录按钮"):
self.webDriver.find_element(By.CSS_SELECTOR, "#submit").click()
with allure.step("验证显示警告信息"):
fail02 = self.webDriver.find_element(By.CSS_SELECTOR, "body > div.jq-toast-wrap.bottom-right > div > h2").text
time.sleep(0.5)
screenshot_helper(f"test_login_failed04")
assert fail02 == expected_msg, f"期望 '{expected_msg}',实际 '{fail02}'"
@allure.story("登录成功")
@allure.title("使用正确的用户名和密码登录")
def test_login_suc(self):
"""检查登录功能 - 成功登录"""
with allure.step("刷新页面"):
self.webDriver.refresh()
with allure.step("输入用户名 testName_001"):
self.webDriver.find_element(By.CSS_SELECTOR, "#username").click()
self.webDriver.find_element(By.CSS_SELECTOR, "#username").send_keys("testName_001")
with allure.step("输入密码 123456"):
self.webDriver.find_element(By.CSS_SELECTOR, "#password").click()
self.webDriver.find_element(By.CSS_SELECTOR, "#password").send_keys("123456")
with allure.step("点击登录按钮"):
self.webDriver.find_element(By.CSS_SELECTOR, "#submit").click()
with allure.step("验证登录成功 - 检查logo"):
self.webDriver.find_element(By.CSS_SELECTOR,
"body > div.page > header.navbar.navbar-expand-md.navbar-light.d-print-none > div > h1 > a > img")
with allure.step("验证登录成功 - 检查发布帖子按钮"):
self.webDriver.find_element(By.CSS_SELECTOR,
"#bit-forum-content > div.page-header.d-print-none > div > div > div.col-auto.ms-auto.d-print-none > div > a.btn.btn-primary.d-none.d-sm-inline-block.article_post")
5>.首页 test_home.py
python
import pytest
import allure
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from Util.util import Util
@pytest.mark.order(3)
@allure.feature("首页功能")
class TestHomePage:
@allure.story("未登录访问首页")
@allure.title("未登录时应跳转到登录页面")
def test_not_login_open_home(self, driver):
"""未登录时打开首页"""
with allure.step("清除localStorage模拟未登录状态"):
driver.execute_script("window.localStorage.clear();")
driver.refresh()
with allure.step("等待页面跳转到登录页"):
wait = WebDriverWait(driver, 5)
wait.until(EC.url_contains("sign-in.html"))
current_url = driver.current_url
print(f"跳转后的url页面>:{current_url}")
with allure.step("验证跳转到登录页面"):
expected_url = "http://127.0.0.1:9092/sign-in.html"
assert current_url == expected_url, f"未登录时的页面跳转失败,跳转的url是{current_url}"
allure.attach(current_url, "实际URL", allure.attachment_type.TEXT)
print("未登录时的页面跳转成功")
@allure.story("登录后访问首页")
@allure.title("登录后应正常显示首页内容")
def test_login_open_home(self, driver, login_user):
"""登录时打开首页"""
# 先登录
Util.login()
# 再访问首页
with allure.step("等待首页元素加载"):
wait = WebDriverWait(driver, 3)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#article_list_board_title")))
with allure.step("验证logo显示"):
driver.find_element(By.CSS_SELECTOR,
"body > div.page > header.navbar.navbar-expand-md.navbar-light.d-print-none > div > h1 > a > img")
with allure.step("验证发布帖子按钮显示"):
driver.find_element(By.CSS_SELECTOR,
"#bit-forum-content > div.page-header.d-print-none > div > div > div.col-auto.ms-auto.d-print-none > div > a.btn.btn-primary.d-none.d-sm-inline-block.article_post")
with allure.step("验证首页标题显示"):
driver.find_element(By.CSS_SELECTOR, "#article_list_board_title")
6>.发布帖子 test_publish.py
python
import time
import pytest
import allure
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
@pytest.mark.order(4)
@allure.feature("发布帖子功能")
class TestPublishPage:
def simple_send_editor(self, driver, content):
"""简单操作编辑器"""
editor_container = driver.find_element(By.CSS_SELECTOR,
"#edit-article > div.CodeMirror.cm-s-default.CodeMirror-wrap.CodeMirror-empty > div.CodeMirror-scroll")
ActionChains(driver).double_click(editor_container).send_keys(content).perform()
@allure.story("进入发布页面")
@allure.title("验证发布帖子界面正常显示")
def test_publish_post_page(self, driver, login_user):
"""测试正常进入发布帖子界面"""
with allure.step("点击发布帖子按钮"):
driver.find_element(By.XPATH, '//*[@id="bit-forum-content"]/div[1]/div/div/div[2]/div/a[1]').click()
time.sleep(1)
with allure.step("验证发帖页面元素"):
driver.find_element(By.CSS_SELECTOR,
"#bit-forum-content > div.page-header.d-print-none > div > div > div > h2")
driver.find_element(By.CSS_SELECTOR, "#article_post_submit")
@allure.story("发布失败-无标题")
@allure.title("标题为空时发布应失败")
def test_publish_title_failed(self, driver):
"""发布帖子失败(标题没有)"""
with allure.step("只输入内容不输入标题"):
self.simple_send_editor(driver, "标题测试失败")
with allure.step("点击发布按钮"):
publish_button = driver.find_element(By.XPATH, '//*[@id="article_post_submit"]')
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});",
publish_button)
time.sleep(1)
publish_button.click()
with allure.step("验证仍在发帖界面(发帖失败)"):
driver.find_element(By.CSS_SELECTOR,
"#bit-forum-content > div.page-header.d-print-none > div > div > div > h2")
time.sleep(1)
@allure.story("发布失败-无内容")
@allure.title("内容为空时发布应失败")
def test_publish_content_fail(self, driver):
"""测试发布帖子失败(内容没有)"""
with allure.step("刷新页面并点击发布按钮"):
driver.refresh()
time.sleep(1)
element = driver.find_element(By.CSS_SELECTOR,
"#bit-forum-content > div.page-header.d-print-none > div > div > div.col-auto.ms-auto.d-print-none > div > a.btn.btn-primary.d-none.d-sm-inline-block.article_post")
element.click()
with allure.step("只输入标题不输入内容"):
driver.find_element(By.CSS_SELECTOR, "#article_post_title").send_keys("标题")
with allure.step("点击发布按钮"):
publish_button = driver.find_element(By.XPATH, '//*[@id="article_post_submit"]')
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});",
publish_button)
time.sleep(1)
publish_button.click()
with allure.step("验证仍在发帖界面(发帖失败)"):
driver.find_element(By.CSS_SELECTOR,
"#bit-forum-content > div.page-header.d-print-none > div > div > div > h2")
time.sleep(1)
@allure.story("发布成功")
@allure.title("输入标题和内容后发布成功")
@pytest.mark.parametrize("title,content", [
("测试标题001", "你好,测试"),("方便在主页点击定位元素","你好呀,看主页的人")
])
def test_publish_post_suc(self, driver, title, content):
"""测试正常发布帖子功能"""
with allure.step("刷新并点击发布按钮"):
driver.refresh()
wait = WebDriverWait(driver, 3)
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR,
"#bit-forum-content > div.page-header.d-print-none > div > div > div.col-auto.ms-auto.d-print-none > div > a.btn.btn-primary.d-none.d-sm-inline-block.article_post")))
element = driver.find_element(By.CSS_SELECTOR,
"#bit-forum-content > div.page-header.d-print-none > div > div > div.col-auto.ms-auto.d-print-none > div > a.btn.btn-primary.d-none.d-sm-inline-block.article_post")
element.click()
with allure.step(f"选择板块并输入标题:{title}"):
driver.find_element(By.CSS_SELECTOR, "#article_post_borad")
driver.find_element(By.CSS_SELECTOR, "#article_post_borad > option:nth-child(7)").click()
driver.find_element(By.CSS_SELECTOR, "#article_post_title").click()
driver.find_element(By.CSS_SELECTOR, "#article_post_title").send_keys(title)
with allure.step(f"输入内容:{content}"):
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#edit-article > div.CodeMirror")))
self.simple_send_editor(driver, content)
with allure.step("点击发布"):
publish_button = driver.find_element(By.XPATH, '//*[@id="article_post_submit"]')
driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});",
publish_button)
time.sleep(1)
publish_button.click()
time.sleep(1)
7>.帖子详情界面 test_details.py
python
import time
import pytest
import allure
from selenium.common.exceptions import StaleElementReferenceException
from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.wait import WebDriverWait
from Util.util import Util
@pytest.mark.order(5)
@allure.feature("帖子详情")
class TestDetailsPage:
@allure.story("帖子详情页显示")
def test_details_page_right(self, driver):
"""测试帖子详情页正常显示"""
with allure.step("登录系统"):
# Util.login()
time.sleep(0.1)
with allure.step("导航到招聘信息板块"):
target_post = Util.slide_page("#topBoardList > li:nth-child(8) > a > span.nav-link-title")
target_post.click()
with allure.step("验证帖子列表显示"):
Util.slide_page("#artical-items-body > div:nth-child(1) > div > div.col > div.text-truncate > a > strong")
@allure.story("点赞功能")
def test_thumsup(self, driver, screenshot_helper):
"""点赞操作"""
with allure.step("进入帖子详情"):
driver.find_element(By.CSS_SELECTOR,
"#artical-items-body > div:nth-child(1) > div > div.col > div.text-truncate > a > strong").click()
wait = WebDriverWait(driver, 3)
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#details_btn_like_count")))
with allure.step("获取点赞前的数量"):
per_thums = int(driver.find_element(By.CSS_SELECTOR, "#details_article_likeCount").text)
with allure.step("执行点赞"):
driver.find_element(By.CSS_SELECTOR, "#details_btn_like_count").click()
with allure.step("验证点赞成功"):
new_thums = int(driver.find_element(By.CSS_SELECTOR, "#details_article_likeCount").text)
assert new_thums - per_thums == 1, f"点赞失败,期望增加1,实际增加{new_thums - per_thums}"
conent = WebDriverWait(driver, 10).until(
lambda current_driver: find_visible_toast(current_driver, "成功")
)
assert conent == "成功", f"点赞 toast 提示异常,期望'成功',实际'{conent}'"
time.sleep(0.5)
screenshot_helper(type(self).__name__ + "_test_thumsup")
print("点赞成功")
@allure.story("回复功能")
def test_replay_content(self, driver, screenshot_helper):
"""回复功能"""
with allure.step("滚动到回复按钮"):
Util.slide_page("#details_btn_article_reply")
with allure.step("输入回复内容"):
self._simple_send_editor(driver, "你好,我是回复")
time.sleep(0.5)
with allure.step("提交回复"):
driver.find_element(By.CSS_SELECTOR, "#details_btn_article_reply").click()
with allure.step("验证回复成功"):
wait = WebDriverWait(driver, 3)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "body > div.jq-toast-wrap.bottom-right > div")))
text = driver.find_element(By.XPATH, "/html/body/div[4]/div[2]/h2").text
assert text == "提示", f"回复 toast 提示异常,期望'提示',实际'{text}'"
Util.slide_page("#details_reply_area > div > div.col-9.card.card-lg > div.card-body")
driver.find_element(By.CSS_SELECTOR,
"#details_reply_area > div > div.col-9.card.card-lg > div.card-footer.bg-transparent.mt-auto > div > div > ul > li:nth-child(2) > a")
driver.find_element(By.CSS_SELECTOR,
"#details_reply_area > div > div.col-9.card.card-lg > div.card-footer.bg-transparent.mt-auto > div > div > ul > li:nth-child(1)")
driver.find_element(By.CSS_SELECTOR, "#details_reply_area > div > div.col-3.card > div")
screenshot_helper("回复消息_test_replay_content")
@allure.story("删除回复")
def test_delete_replay(self, driver, screenshot_helper):
"""删除帖子回复"""
delete_reply_selector = (
"#details_reply_area > div > div.col-9.card.card-lg "
"> div.card-footer.bg-transparent.mt-auto > div > div > ul > li:nth-child(2) > a"
)
# 删除操作包含弹窗和异步请求,使用显式等待避免固定 sleep 带来的竞态。
wait = WebDriverWait(driver, 10)
with allure.step("打开删除回复确认框"):
# 等待删除按钮真正可点击,确保回复区域和页面状态已经加载完成。
delete_reply_button = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, delete_reply_selector))
)
screenshot_helper("删除评论之前")
delete_reply_button.click()
with allure.step("确认删除回复"):
# 删除按钮点击后,确认按钮需要等待弹窗渲染完成再操作。
confirm_button = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "#details_artile_replyed_delete"))
)
confirm_button.click()
with allure.step("等待删除结果"):
# 页面可能同时存在多个 toast,不能依赖固定的 div:nth-child() 下标。
# 每次轮询都重新查找元素,避免前端刷新 toast 后引用失效。
suc_content = wait.until(
lambda current_driver: find_visible_toast(current_driver, "成功")
)
assert suc_content == "成功", f"删除回复 toast 提示异常,期望'成功',实际'{suc_content}'"
with allure.step("等待遮罩关闭"):
# 确认删除后仍可能有 modal-backdrop,遮罩消失后页面才允许继续交互
wait.until(
EC.invisibility_of_element_located((By.CSS_SELECTOR, "body > div.modal-backdrop.fade.show"))
)
screenshot_helper("删除评论后")
@allure.story("编辑帖子")
def test_editor_details(self, driver, screenshot_helper):
"""编辑帖子页面"""
time.sleep(0.5)
with allure.step("定位编辑按钮"):
driver.find_element(By.CSS_SELECTOR, "#details_artile_edit")
Util.slide_page("#details_artile_edit")
screenshot_helper("修改帖子前_test_editor_details")
with allure.step("进入编辑页面"):
driver.find_element(By.CSS_SELECTOR, "#details_artile_edit").click()
time.sleep(0.5)
with allure.step("修改标题和内容"):
driver.find_element(By.CSS_SELECTOR,
"#bit-forum-content > div.page-header.d-print-none > div > div > div > h2")
driver.find_element(By.CSS_SELECTOR, "#edit_article_title").send_keys("test")
self._simple_send_editor(driver, "modifyContent",
"#edit_article_content_area > div.CodeMirror.cm-s-default.CodeMirror-wrap > div.CodeMirror-scroll")
with allure.step("提交修改"):
Util.slide_page("#edit_article_submit")
driver.find_element(By.CSS_SELECTOR, "#edit_article_submit").click()
modity = WebDriverWait(driver, 10).until(
lambda current_driver: find_visible_toast(current_driver, "成功")
)
assert modity == "成功", f"修改帖子失败,期望'成功',实际'{modity}'"
with allure.step("验证修改结果"):
driver.find_element(By.CSS_SELECTOR,
"#artical-items-body > div:nth-child(1) > div > div.col > div.text-truncate > a > strong").click()
screenshot_helper("修改帖子之后_test_editor_details")
@allure.story("删除帖子")
def test_delete_page(self, driver, screenshot_helper):
"""删除帖子"""
with allure.step("导航到目标帖子"):
target_post = Util.slide_page("#topBoardList > li:nth-child(8) > a > span.nav-link-title")
target_post.click()
time.sleep(0.5)
screenshot_helper("删除帖子之前的截图_test_delete_page")
with allure.step("进入帖子详情"):
driver.find_element(By.CSS_SELECTOR,
"#artical-items-body > div:nth-child(1) > div > div.col > div.text-truncate > a > strong").click()
with allure.step("点击删除"):
wait = WebDriverWait(driver, 10)
delete_button = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR,
"#bit-forum-content > div.page-body > div > div > div:nth-child(1) > div.col-9.card.card-lg > div.card-footer.bg-transparent.mt-auto.justify-content-end > div > div:nth-child(3) > div > a"))
)
delete_button.click()
confirm_button = wait.until(
EC.element_to_be_clickable((By.CSS_SELECTOR, "#details_artile_delete"))
)
confirm_button.click()
with allure.step("验证删除成功"):
# 删除请求是异步的,每次轮询都重新查找 toast,避免读到旧的空节点。
content = wait.until(
lambda current_driver: find_visible_toast(current_driver, "成功")
)
assert content == "成功", f"删除帖子 toast 提示异常,期望'成功',实际'{content}'"
# toast 出现代表请求成功,但 modal 遮罩可能仍在执行关闭动画。
wait.until(EC.invisibility_of_element_located(
(By.CSS_SELECTOR, "body > div.modal-backdrop.fade.show")
))
target_element = wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#topBoardList > li:nth-child(8) > a")))
target_element.click()
screenshot_helper("删除帖子后_test_delete_page")
def _simple_send_editor(self, driver, str_, selector=None):
"""编辑器输入辅助方法"""
if selector is None:
selector = "#article_details_reply > div.CodeMirror.cm-s-default.CodeMirror-wrap.CodeMirror-empty > div.CodeMirror-scroll"
editor_container = driver.find_element(By.CSS_SELECTOR, selector)
actions = ActionChains(driver)
actions.double_click(editor_container)
if selector is not None and selector != "#article_details_reply > div.CodeMirror.cm-s-default.CodeMirror-wrap.CodeMirror-empty > div.CodeMirror-scroll":
actions.key_down(Keys.CONTROL)
actions.send_keys("a")
actions.key_up(Keys.CONTROL)
actions.send_keys(str_)
actions.perform()
TOAST_TITLE_SELECTOR = "body > div.jq-toast-wrap.bottom-right > div > h2"
def find_visible_toast(driver, expected_text):
"""查找当前可见且文本匹配的 toast,兼容 toast 动态刷新。"""
for toast in driver.find_elements(By.CSS_SELECTOR, TOAST_TITLE_SELECTOR):
try:
if toast.is_displayed() and toast.text.strip() == expected_text:
return toast.text.strip()
except StaleElementReferenceException:
# toast 可能在遍历过程中被前端替换,交给 WebDriverWait 下一轮重新查找。
return False
return False
8>.个人中心界面 test_personal_center.py
python
import time
import pytest
import allure
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
@pytest.mark.order(6)
@allure.feature("个人中心")
class TestPersonalCenterPage:
PERSONAL_CENTER_URL = "http://127.0.0.1:9092/index.html"
AVATAR_SELECTOR = (
"body > div.page > header.navbar.navbar-expand-md.navbar-light.d-print-none "
"> div > div > div.nav-item.dropdown > a"
)
def _open_personal_center(self, driver):
"""重新打开个人中心,避免测试之间依赖上一个测试的页面状态。"""
driver.get(self.PERSONAL_CENTER_URL)
wait = WebDriverWait(driver, 10)
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, self.AVATAR_SELECTOR))).click()
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#index_user_settings"))).click()
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#settings_submit_password")))
return wait
@allure.story("个人中心页面显示")
def test_personal_center_page_right(self, driver, screenshot_helper):
"""测试个人中心页面是否正常显示"""
wait = self._open_personal_center(driver)
with allure.step("验证个人中心元素"):
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#setting_submit_nickname")))
driver.find_element(By.CSS_SELECTOR, "#settings_nickname")
screenshot_helper(type(self).__name__ + "_test_personal_center_page_right")
@allure.story("修改基本信息")
@pytest.mark.parametrize("nickname,phone_number", [
("test_modify_info", "17788889999")
])
def test_modify_info_suc(self, driver, screenshot_helper, slide_to_element, nickname, phone_number):
"""测试修改基本信息"""
self._open_personal_center(driver)
with allure.step(f"输入用户昵称: {nickname}"):
nickname_input = driver.find_element(By.CSS_SELECTOR, "#setting_input_nickname")
nickname_input.clear()
nickname_input.send_keys(nickname)
driver.find_element(By.CSS_SELECTOR, "#setting_submit_nickname").click() # 提交修改
# 截图
screenshot_helper("修改用户昵称之后_test_modify_info_suc")
with allure.step("修改邮箱"):
# 先定位到邮箱输入框
email = slide_to_element("#setting_input_email")
email.clear()
# 输入邮箱
email.send_keys("666666@qq.com")
# 提交修改
driver.find_element(By.CSS_SELECTOR, "#setting_submit_email").click()
# 截图
screenshot_helper("修改邮箱之后_test_modify_info_suc")
with allure.step(f"输入电话号码: {phone_number}"):
time.sleep(0.5)
# 手机号输入框和提交按钮是两个不同元素,不能把 submit 按钮当成输入框清空。
phone_element = slide_to_element("#setting_input_phoneNum")
phone_element.clear()
phone_element.send_keys(phone_number)
driver.find_element(By.CSS_SELECTOR, "#setting_submit_phoneNum").click() # 提交修改
screenshot_helper("修改电话号码之后_test_modify_info_suc")
with allure.step("修改个人简历信息"):
time.sleep(0.5)
resume_element = slide_to_element("#settings_textarea_remark")
resume_element.clear()
resume_element.send_keys("测试修改个人简历")
driver.find_element(By.CSS_SELECTOR, "#settings_submit_remark").click() # 提交修改
screenshot_helper("修改个人简历信息之后_test_modify_info_suc")
@allure.story("修改密码")
@pytest.mark.parametrize("old_password,new_password,confirm_password", [
("123456", "654321", "654321")
])
def test_modify_password_suc(self, driver, screenshot_helper, slide_to_element, old_password, new_password, confirm_password):
"""测试修改密码"""
self._open_personal_center(driver)
# 先下滑到密码输入框
slide_to_element("#settings_submit_password")
# 输入旧密码
driver.find_element(By.CSS_SELECTOR, "#settings_input_oldPassword").send_keys(old_password)
with allure.step(f"输入新密码: {new_password},输入确认密码: {confirm_password}"):
driver.find_element(By.CSS_SELECTOR, "#settings_input_newPassword").send_keys(new_password)
driver.find_element(By.CSS_SELECTOR, "#settings_input_passwordRepeat").send_keys(confirm_password)
# 提交修改
driver.find_element(By.CSS_SELECTOR, "#settings_submit_password").click()
current_time = time.time()
# 跳转至登录界面
time.sleep(1.5)
# 计算时间并打印
time_diff = time.strftime("%Y%m%d%H%M%S", time.localtime(time.time() - current_time))
print(f"修改密码耗时: {time_diff} 秒")
screenshot_helper("修改密码之后_test_modify_password_suc")
9>.我的帖子界面 test_my_post.py
python
import time
import pytest
import allure
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from Util.util import Util
@pytest.mark.order(7)
@allure.feature("我的帖子")
class TestMyPostPage:
@allure.story("我的帖子页面显示")
def test_my_post_page_right(self, driver):
"""测试我的帖子界面是否正常显示"""
with allure.step("使用新密码登录"):
Util.login(username="testName_001", password="654321")
wait = WebDriverWait(driver, 3)
with allure.step("点击右上角头像"):
element = driver.find_element(By.CSS_SELECTOR,
"body > div.page > header.navbar.navbar-expand-md.navbar-light.d-print-none > div > div > div.nav-item.dropdown > a")
element.click()
with allure.step("点击我的帖子"):
wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, "#index_user_profile")))
driver.find_element(By.CSS_SELECTOR, "#index_user_profile").click()
with allure.step("验证进入我的帖子界面"):
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#index_user_profile")))
@allure.story("帖子详情访问")
def test_detail_page_right(self, driver):
"""测试帖子详情是否可以正常访问"""
with allure.step("等待并点击进入帖子详情页"):
wait = WebDriverWait(driver, 3)
time.sleep(2)
driver.find_element(By.CSS_SELECTOR,
"#profile_article_body > li:nth-child(1) > div.card.timeline-event-card > div > div > div > div > div.text-truncate > a").click()
with allure.step("验证详情页元素"):
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#details_article_title")))
driver.find_element(By.CSS_SELECTOR, "#details_article_title")
driver.find_element(By.CSS_SELECTOR, "#details_btn_like_count")
10>.站内信 test_message.py
python
import time
import pytest
import allure
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from Util.util import Util
@pytest.mark.order(8)
@allure.feature("站内信功能")
class TestMessagePage:
@allure.story("发私信界面显示")
def test_post_message_right(self, driver,slide_to_element):
"""测试发私信界面是否正常显示"""
# driver.get(TestMessagePage.URL)
with allure.step("等待首页加载"):
wait = WebDriverWait(driver, 3)
wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "#navbar-menu > div > div")))
with allure.step("导航到CPP帖子"):
target = slide_to_element("#topBoardList > li:nth-child(3) > a > span.nav-link-title")
target.click()
time.sleep(0.5)
# 进入第一个帖子
title = slide_to_element("#artical-items-body > div:nth-child(1) > div > div.col > div.text-truncate > a > strong")
# 点击
title.click()
with allure.step("打开发私信界面"):
driver.find_element(By.CSS_SELECTOR, "#btn_details_send_message").click()
driver.find_element(By.CSS_SELECTOR,
"#index_message_modal > div > div > div.modal-header > h5")
driver.find_element(By.CSS_SELECTOR, "#btn_index_send_message")
time.sleep(0.5)
with allure.step("关闭发私信窗口"):
driver.find_element(By.CSS_SELECTOR, "#index_message_receive_cancel").click()
@allure.story("发送私信")
@pytest.mark.parametrize("message_content", [
("你好,测试人员")
])
def test_post_message_suc(self, driver, message_content, slide_to_element,screenshot_helper):
"""测试成功发送私信"""
with allure.step("打开发送私信窗口"):
driver.find_element(By.CSS_SELECTOR, "#btn_details_send_message").click()
time.sleep(0.5)
with allure.step(f"输入私信内容: {message_content}"):
driver.find_element(By.CSS_SELECTOR, "#index_message_receive_content").send_keys(message_content)
with allure.step("点击发送"):
time.sleep(1)
driver.find_element(By.CSS_SELECTOR, "#btn_index_send_message").click()
time.sleep(1)
# 后续要退出,此时上划到头像处
avatar = slide_to_element ("#index_nav_avatar")
avatar.click()
# 点击退出
driver.find_element(By.CSS_SELECTOR, "#index_user_logout").click()
@allure.story("回复站内信")
@pytest.mark.parametrize("sender,password,reply_content", [
("202691", "123456", "你好呀,这只是一个测试回复,baibai~")
])
def test_replay_message_page(self, driver, sender, password, reply_content, slide_to_element,screenshot_helper):
"""回复站内信"""
# with allure.step("退出当前登录"):
# util_instance = Util(driver.current_url)
# util_instance.exit_login()
with allure.step(f"使用用户 {sender} 登录"):
Util.login(sender, password)
with allure.step("打开站内信"):
driver.find_element(By.CSS_SELECTOR,
"body > div.page > header.navbar.navbar-expand-md.navbar-light.d-print-none > div > div > div:nth-child(2) > div > a > svg").click()
driver.find_element(By.CSS_SELECTOR,
"#index_div_message_list > div:nth-child(1) > div > div.col.text-truncate > a").click()
with allure.step("点击回复"):
driver.find_element(By.CSS_SELECTOR, "#btn_index_message_reply").click()
with allure.step(f"输入回复内容: {reply_content}"):
time.sleep(0.5)
driver.find_element(By.CSS_SELECTOR, "#index_message_reply_receive_content").send_keys(reply_content)
driver.find_element(By.CSS_SELECTOR, "#btn_index_send_message_reply").click()
with allure.step("验证回复成功"):
page_content = driver.find_element(By.CSS_SELECTOR, "body > div.jq-toast-wrap.bottom-right > div").text
# AssertionError: assert '×\n成功\n操作成功' == '成功'
assert page_content.split("\n")[1] == "成功"
@allure.story("查看站内信")
def test_check_page(self, driver, screenshot_helper,slide_to_element):
"""查看是否接受了"""
with allure.step("关闭侧边栏"):
driver.find_element(By.CSS_SELECTOR,
"#index_message_offcanvasEnd > div.offcanvas-header > button").click()
with allure.step("退出并重新登录"):
avatar = slide_to_element("#index_nav_avatar")
avatar.click()
# 点击退出
time.sleep(1)
driver.find_element(By.CSS_SELECTOR, "#index_user_logout > span").click()
Util.login("testName_001", "654321")
with allure.step("打开站内信"):
driver.find_element(By.CSS_SELECTOR,
"body > div.page > header.navbar.navbar-expand-md.navbar-light.d-print-none > div > div > div:nth-child(2) > div > a > svg").click()
driver.find_element(By.CSS_SELECTOR,
"#index_div_message_list > div:nth-child(1) > div > div.col.text-truncate > a").click()
with allure.step("验证收到回复内容"):
time.sleep(0.5)
content = driver.find_element(By.CSS_SELECTOR, "#index_message_detail_content").text
assert content == "你好呀,这只是一个测试回复,baibai~"
3.测试报告分析

因为时间问题,这里的测试用例只有32 个,即覆盖了基本的体验流程与,可以看到基本流程是全部pass,后续仍会扩展保证测试用例的质量 与覆盖面
三.总结
本次迭代在上一轮 Selenium+JMeter 简单测试的基础上,结合 AI 对博客论坛系统完成了两轮自动化测试升级:
- 接口自动化:基于需求文档整理出覆盖注册、登录、首页、发帖、帖子详情、个人中心、站内信等全流程的接口文档,并在 AI 辅助下使用 pytest + requests + jsonschema + Allure 编写测试。共执行 127 个用例,111 个通过,12 个失败且均对应真实缺陷,集中在**基础参数校验缺失、长度溢出与底层异常处理不当、业务逻辑漏洞(含越权风险)**三类,均已给出优化建议;另有 4 个 NA
- WebUI 测试:将原先"创建类、实例化对象逐个运行"的 Java 脚本重构为 pytest 框架,引入全局 fixture、失败自动截图、日志按级别与日期分割、Allure 报告等机制。当前 32 个用例覆盖注册、登录、首页、发布、详情、个人中心、我的帖子、站内信等基本体验流程,全部通过
存在的缺点:
-
系统缺陷(由 12 个失败用例暴露):
一是基础参数校验缺失,多个接口未拦截空值、非法格式与非法枚举,脏数据可直接入库
二是长度溢出与底层异常处理不当,超长参数未做长度限制,站内信不存在 ID 直接抛空指针异常
三是业务逻辑漏洞,发信未校验接收用户是否存在、可给自己发信,且存在越权风险------非信件接收方可直接回复他人站内信。
-
测试工程不足:
一 接口用例 127 个、WebUI 用例仅 32 个,数量与覆盖面仍不够
二 关键性质量校验测试用例不足,如>:弱网测试、sql注入、敏感字段校验等都未涉及,项目仍存在很大隐私泄露风险
-
AI 辅助问题 :AI 生成存在脚本目录缺失、文件夹出现后缀、依赖版本过高导致结果收集失败、AI 幻觉(错误提示中出现架构不存在的包名)等问题,每一步 AI 生成结果都需人工review
后续计划:
- 继续扩充测试用例的数量与覆盖面,保证用例质量,如>:弱网测试、sql注入、敏感字段校验
- 缺陷修复与优化建议:Controller/Service 层补齐参数校验与异常拦截,补做数据存在性与所属权校验,持续保障系统质量