0 测试结果
接口测试:采用 Pytest + Requests 进行测试,验证了底层数据流转与高并发场景下的播放量累加逻辑。设计 46 个正逆向测试用例,核心逻辑覆盖率达 85% 以上,测试期间发现并修复 7 个 Bug;
UI 测试:使用 Playwright 框架进行测试,实现了 4 大核心业务链路(涵盖视频增删改查闭环、搜索与评论动态回显等)的场景覆盖。用例内部拆解了 30+ 个强交互断言节点;
1 测试脚本
1.1 基于 Requests 的接口自动化测试
在现代 Web 开发中,前后端分离架构使得 API 成为了系统的命脉。本小节主要使用 Pytest + Requests 库,对在线点播系统后端接口进行测试。
1.1.0 接口自动化文档 (涵盖 46 个核心测试用例)
-
test_video_list_and_search(C端搜索与列表接口)- 测试目的:验证主页视频列表接口 (
/api/connect_PgSql) 的可用性、搜索过滤准确性以及恶意输入防御能力。 - 核心逻辑:参数化驱动 5 个用例(全量查询、正常关键字、特殊字符、超长边界字符串、SQL 注入探测),断言 HTTP 状态码为 200,并校验返回格式为 List。
- 设计:针对 SQL 注入(如
' OR '1'='1)和特殊字符进行深度安全断言,强制验证当遭遇恶意攻击负载时,后端能有效拦截并返回空列表,避免数据库报错或数据泄露。
- 测试目的:验证主页视频列表接口 (
-
test_get_video_details(视频详情与资源状态路由校验)- 测试目的:验证单个视频详情接口 (
/api/get_video_details) 的数据获取完整性,以及防盗链机制与非数字 ID 拦截防线。 - 核心逻辑:参数化驱动 5 个用例(有效 ID、超大不存在 ID、负数 ID、字符串注入、空参)。断言有效 ID 返回 200,不存在 ID 返回 404,非法格式前置拦截返回 400。
- 设计:通过
RUNTIME_DATA动态提取真实有效 ID 避免硬编码;并深度断言 JSON 结构中包含signed_play_url,确保 MD5 时间戳防盗链签名被正确计算与下发。
- 测试目的:验证单个视频详情接口 (
-
test_add_comment_boundaries&test_add_comment_method_not_allowed(新增评论边界与方法拦截)- 测试目的:严格验证评论发布接口 (
/api/add_comment) 的输入边界、跨站脚本 (XSS) 容忍度以及 HTTP 方法合规性。 - 核心逻辑:参数化驱动 9 个用例(包含正常发布、空内容、纯空格、XSS 盲打
<script>、2000字超长压力测试、缺少外键 ID,以及 GET/PUT/DELETE 请求方法阻断)。 - 设计:引入了
time.sleep(0.2)缓冲机制,有效解决瞬时极高并发(瞬间发起数十个带有大 payload 请求)导致的 Serverless 数据库连接池被打满问题;同时精准验证了 405 Method Not Allowed 的 RESTful 语义规范。
- 测试目的:严格验证评论发布接口 (
-
test_update_view_methods&test_update_view_exceptions(播放量更新并发与异常探测)- 测试目的:验证底层播放量
+1运算接口 (/api/update_view) 对多种 HTTP 动词的支持及对非法参数的隔离。 - 核心逻辑:参数化驱动 6 个用例。验证 GET 和 POST 双方法均能返回 204 No Content;针对不存在 ID 严格断言 404(通过 SQL 的 RETURNING 长度判断),针对负数和非法字符串断言 400。
- 测试目的:验证底层播放量
-
test_manage_videos_pagination_and_search(后台列表分页与边界容错测试)- 测试目的:验证管理后台视频列表 (
/api/manage-videos) 的分页、联合搜索逻辑,以及防止内存溢出 (OOM) 的兜底防线。 - 核心逻辑:参数化驱动 6 个用例(标准请求、分页叠加搜索、page=0、page=-1、超大 limit 值、注入非法字符串)。
- 设计:极致的后端容错探测。当故意传入
limit=99999时,不仅断言返回 200,更通过len(videos) <= 10的深度断言,验证后端是否成功触发了强制修正逻辑,完美防御了黑客恶意拉跨数据库的攻击。
- 测试目的:验证管理后台视频列表 (
-
test_manage_comments(多维检索与 CRUD 数据清理闭环)- 测试目的:验证评论后台 (
/api/manage-comments) 的双向联合检索能力,以及自动化测试框架的“造数据-查数据-删数据”闭环扫尾能力。 - 核心逻辑:参数化检索和异常删除共 9 个用例(评论/视频单维搜索、联合搜索、注入防御,以及缺失/越界/非法 ID 删除)。外加 1 个完整的生命周期闭环测试。
- 设计:实现了测试数据零残留。通过 POST 接口先造出一条带有“测试靶点”特征的脏数据,调用 GET 接口利用模糊搜索动态锁定它的
comment_id,再调用 DELETE 接口物理抹除。最后再发一次 DELETE 请求断言返回 404,严格验证了接口的幂等性和数据销毁的彻底性。
- 测试目的:验证评论后台 (
-
test_analytics_methods_and_data(后台统计大屏聚合接口)- 测试目的:验证统计面板聚合 API (
/api/analytics) 的并发查询有效性及数据结构合规性。 - 核心逻辑:参数化驱动 4 个用例(GET 数据获取,以及 POST/PUT/DELETE 的 405 拦截)。
- 设计:针对 200 成功的请求进行深度结构校验,严格断言
topVideos、topTagsByViews和topTagsByCount三个板块不仅存在,且数据类型必须为合法的 List 列表。
- 测试目的:验证统计面板聚合 API (
-
test_get_view_counts_structure(全局播放量缓存击穿与结构断言)- 测试目的:验证全站播放量字典接口 (
/api/get_view_counts) 的返回格式与缓存穿透能力。 - 核心逻辑:执行 1 个核心用例。在 URL 末尾动态拼接毫秒级时间戳(
?_t=xxx)强行绕过 Vercel 边缘缓存,确保拿到数据库绝对实时数据。
- 测试目的:验证全站播放量字典接口 (
1.1.1 接口自动化测试脚本
import pytest
import requests
import time
# 全局配置
BASE_URL = "https://xxx.cn" # 如果是本地环境,请替换为 http://localhost:3000;否则 https://xxx.cn
TIMEOUT = 20
# 模块级全局变量,用于存储动态获取的依赖数据
RUNTIME_DATA = {
"valid_video_id": None,
"valid_comment_id": None
}
@pytest.fixture(scope="module", autouse=True)
def setup_test_data():
"""
前置夹具 (Fixture):在所有测试开始前,先获取一个真实的 video_id,
供后续所有需要 videoId 的异常测试使用。
"""
resp = requests.get(f"{BASE_URL}/api/connect_PgSql", timeout=TIMEOUT)
if resp.status_code == 200 and len(resp.json()) > 0:
RUNTIME_DATA["valid_video_id"] = resp.json()[0]["id"]
else:
pytest.skip("严重警告:数据库中没有视频,无法进行后续依赖视频的测试。")
class TestComprehensiveAPIs:
# =======================================================
# 模块 1:C端搜索与列表接口 (累计 5 个用例)
# 路由: /api/connect_PgSql
# =======================================================
@pytest.mark.parametrize("search_term, expected_status", [
("", 200), # 用例 1: 正常全量查询
("风景", 200), # 用例 2: 正常关键字查询
("!@#$%^&*()", 200), # 用例 3: 特殊字符查询 (测试 SQL 注入防御和崩溃)
("A" * 500, 200), # 用例 4: 超长字符串边界测试
("' OR '1'='1", 200), # 用例 5: 经典 SQL 注入探测
])
def test_video_list_and_search(self, search_term, expected_status):
url = f"{BASE_URL}/api/connect_PgSql"
params = {"search": search_term} if search_term else {}
resp = requests.get(url, params=params, timeout=TIMEOUT)
assert resp.status_code == expected_status
data = resp.json()
assert isinstance(data, list)
# 进阶断言:如果是乱码或注入,结果必须是空列表
if search_term in ["!@#$%^&*()", "A" * 500, "' OR '1'='1"]:
assert len(data) == 0, f"警告:搜索 {search_term} 竟然查出了数据!可能存在漏洞!"
# =======================================================
# 模块 2:视频详情与防盗链接口 (累计 5 个用例)
# 路由: /api/get_video_details
# =======================================================
@pytest.mark.parametrize("video_id, expected_status, expect_error", [
("VALID_ID", 200, False), # 用例 6: 正常 ID
("99999999", 404, True), # 用例 7: 不存在的极大数据
("-1", 400, True), # 用例 8: 负数 ID
("abc", 400, True), # 用例 9: 字符串非法类型注入
("", 400, True), # 用例 10: 空参拦截测试
])
def test_get_video_details(self, video_id, expected_status, expect_error):
# 动态替换有效的 ID
vid = RUNTIME_DATA["valid_video_id"] if video_id == "VALID_ID" else video_id
url = f"{BASE_URL}/api/get_video_details?id={vid}"
resp = requests.get(url, timeout=TIMEOUT)
assert resp.status_code == expected_status
if expect_error:
assert "error" in resp.json()
else:
data = resp.json()
assert data["id"] == vid
assert "signed_play_url" in data # 强校验防盗链 URL 是否生成
# =======================================================
# 模块 3:新增评论接口 (累计 6 个用例)
# 路由: /api/add_comment
# =======================================================
@pytest.mark.parametrize("use_valid_vid, content, expected_status", [
(True, "正常测试评论", 201), # 用例 11: 正常发布
(True, "", 400), # 用例 12: 内容为空阻断
(True, " ", 400), # 用例 13: 纯空格阻断
(True, "<script>alert('xss')</script>", 201), # 用例 14: XSS 盲打测试 (后端应允许存入,前端转义)
(True, "A" * 2000, 201), # 用例 15: 超长文本压力测试
(False, "缺少视频ID", 400), # 用例 16: 必填外键丢失
])
def test_add_comment_boundaries(self, use_valid_vid, content, expected_status):
url = f"{BASE_URL}/api/add_comment"
payload = {"content": content}
if use_valid_vid:
payload["videoId"] = RUNTIME_DATA["valid_video_id"]
# 💡 解决方案 1:每次请求前微微停顿 0.2 秒,给 Neon 数据库喘息和释放连接的时间
time.sleep(0.2)
resp = requests.post(url, json=payload, timeout=TIMEOUT)
assert resp.status_code == expected_status
# =======================================================
# 模块 4:播放量递增并发与缓存击穿 (累计 6 个用例)
# 路由: /api/update_view
# =======================================================
@pytest.mark.parametrize("method", ["GET", "POST"]) # 用例 17, 18: 支持双请求方法
def test_update_view_methods(self, method):
vid = RUNTIME_DATA["valid_video_id"]
url = f"{BASE_URL}/api/update_view?id={vid}"
resp = requests.request(method, url, timeout=TIMEOUT)
assert resp.status_code == 204
# 精准断言资源不存在 (404) 与类型非法 (400)
@pytest.mark.parametrize("invalid_id, expected_status", [
("99999999", 404), # 用例 19: 更新不存在的ID,必须返回 404
("abc", 400), # 用例 20: 非法字符 ID,必须前置拦截返回 400
("-5", 400), # 用例 21: 负数 ID 拦截
("", 400), # 用例 22: 丢失 ID 阻断
])
def test_update_view_exceptions(self, invalid_id, expected_status):
url = f"{BASE_URL}/api/update_view?id={invalid_id}"
resp = requests.get(url, timeout=TIMEOUT)
assert resp.status_code == expected_status
# =======================================================
# 模块 5:后台管理 - 列表分页边界测试 (累计 6 个用例)
# 路由: /api/manage-videos (GET)
# =======================================================
# 分页与搜索组合测试
@pytest.mark.parametrize("page, limit, search, expected_status", [
(1, 5, "", 200), # 用例 23: 标准请求
(2, 5, "风景", 200), # 用例 24: (新增) 分页叠加搜索条件
(0, 5, "", 200), # 用例 25: page 为 0 (后端应自动纠正为 1)
(-1, 5, "", 200), # 用例 26: page 为负数 (后端应自动纠正为 1)
(1, 99999, "", 200), # 用例 27: 极大 limit (后端应自动限制为 10)
("abc", "xyz", "", 200), # 用例 28: 类型注入 (后端应 fallback 到默认值 1 和 5)
])
def test_manage_videos_pagination_and_search(self, page, limit, search, expected_status):
url = f"{BASE_URL}/api/manage-videos?page={page}&limit={limit}&search={search}"
resp = requests.get(url, timeout=TIMEOUT)
assert resp.status_code == expected_status
data = resp.json()
assert "videos" in data and "pagination" in data
# =======================================================
# 模块 6:评论管理与数据闭环扫尾 (累计 10 个用例)
# 路由: /api/manage-comments (GET, DELETE)
# =======================================================
@pytest.mark.parametrize("comment_search, video_search", [
("", ""), # 用例 29: 全量
("测试", ""), # 用例 30: 仅搜评论
("", "风景"), # 用例 31: 仅搜视频
("测试", "风景"), # 用例 32: 联合检索
("'; DROP TABLE comments;--", ""), # 用例 33: 联合检索 SQL 注入防御
])
def test_manage_comments_search(self, comment_search, video_search):
url = f"{BASE_URL}/api/manage-comments?commentSearch={comment_search}&videoSearch={video_search}"
resp = requests.get(url, timeout=TIMEOUT)
assert resp.status_code == 200
assert "comments" in resp.json()
@pytest.mark.parametrize("comment_id, expected_status", [
("", 400), # 用例 34: 缺省ID
("99999999", 404), # 用例 35: 不存在的ID
("abc", 400), # 用例 36: 类型错误
("-1", 400), # 用例 37: 类型错误
])
def test_manage_comments_delete_exceptions(self, comment_id, expected_status):
url = f"{BASE_URL}/api/manage-comments?commentId={comment_id}"
resp = requests.delete(url, timeout=TIMEOUT)
assert resp.status_code == expected_status
def test_manage_comments_delete_success(self):
"""
用例 38: 完整的评论删除闭环
先造一条脏数据,获取它的 ID,然后执行删除,最后断言找不到了。
"""
# 1. 造数据
vid = RUNTIME_DATA["valid_video_id"]
post_resp = requests.post(f"{BASE_URL}/api/add_comment", json={"videoId": vid, "content": "【删除闭环测试】靶点"})
assert post_resp.status_code == 201
# 2. 查出它的 ID
get_resp = requests.get(f"{BASE_URL}/api/manage-comments?commentSearch=【删除闭环测试】靶点")
comments = get_resp.json().get("comments", [])
assert len(comments) > 0
target_id = comments[0]["comment_id"]
# 3. 执行删除
del_resp = requests.delete(f"{BASE_URL}/api/manage-comments?commentId={target_id}")
assert del_resp.status_code == 200
# 4. 再次删除同一条 (测试幂等性/404)
del_again_resp = requests.delete(f"{BASE_URL}/api/manage-comments?commentId={target_id}")
assert del_again_resp.status_code == 404
# =======================================================
# 模块 7:增加 HTTP 请求方法拦截测试 (累计 3 个用例)
# 路由: /api/add_comment
# =======================================================
@pytest.mark.parametrize("method, expected_status", [
("GET", 405),# 用例 39: 类型错误
("PUT", 405),# 用例 40: 类型错误
("DELETE", 405), # 用例 41: 类型错误
])
def test_add_comment_method_not_allowed(self, method, expected_status):
url = f"{BASE_URL}/api/add_comment"
resp = requests.request(method, url, timeout=TIMEOUT)
assert resp.status_code == expected_status
# =======================================================
# 模块 8:后台统计面板接口 (累计 4 个用例)
# 路由: /api/analytics
# =======================================================
@pytest.mark.parametrize("method, expected_status", [
("GET", 200), # 用例 42: 正常获取数据
("POST", 405), # 用例 43: 方法不被允许
("PUT", 405), # 用例 44: 方法不被允许
("DELETE", 405), # 用例 45: 方法不被允许
])
def test_analytics_methods_and_data(self, method, expected_status):
url = f"{BASE_URL}/api/analytics"
resp = requests.request(method, url, timeout=TIMEOUT)
assert resp.status_code == expected_status
# 对于 200 成功的请求,深度断言其返回的 JSON 结构
if expected_status == 200:
data = resp.json()
assert "topVideos" in data, "缺少 topVideos 字段"
assert "topTagsByViews" in data, "缺少 topTagsByViews 字段"
assert "topTagsByCount" in data, "缺少 topTagsByCount 字段"
# 进一步断言:这三个字段的值必须是列表
assert isinstance(data["topVideos"], list)
assert isinstance(data["topTagsByViews"], list)
assert isinstance(data["topTagsByCount"], list)
# =======================================================
# 模块 9:全站播放量字典接口 (累计 1 个用例)
# 路由: /api/get_view_counts
# =======================================================
def test_get_view_counts_structure(self):
"""
用例 46: 正常获取全站播放量字典,并断言其为合法的 Dictionary 对象
"""
url = f"{BASE_URL}/api/get_view_counts"
# 加上时间戳击穿缓存,确保拿到的是数据库实时结构
timestamp = int(time.time() * 1000)
resp = requests.get(f"{url}?_t={timestamp}", timeout=TIMEOUT)
assert resp.status_code == 200
data = resp.json()
# 核心断言:必须是一个字典 { "1": 100, "2": 50 }
assert isinstance(data, dict), "全站播放量返回的数据结构必须是字典(dict)"
# 边界抽样测试:如果字典不为空,随便取一个值看看是不是整数(或者能转成整数的字符串)
if len(data) > 0:
random_value = list(data.values())[0]
try:
int(random_value)
except ValueError:
pytest.fail(f"播放量的值不合法,应该是数字,实际是: {random_value}")
1.1.2 pytest-html 测试报告
pytest -s -v --html=report.html --self-contained-html 40_tets.py
# 运行当前文件并生成report.html测试报告
PS F:\my-video-app> pytest -s -v --html=report.html --self-contained-html 40_tets.py
======================================================== test session starts ========================================================
platform win32 -- Python 3.7.8, pytest-7.4.4, pluggy-1.2.0 -- E:\python\python.exe
cachedir: .pytest_cache
metadata: {'Python': '3.7.8', 'Platform': 'Windows-10-10.0.22621-SP0', 'Packages': {'pytest': '7.4.4', 'pluggy': '1.2.0'}, 'Plugins': {'allure-pytest': '2.15.3', 'anyio': '3.7.1', 'base-url': '2.0.0', 'cov': '4.1.0', 'html': '3.2.0', 'metadata': '3.0.0', 'playwright': '0.3.3'}, 'JAVA_HOME': 'F:\\jdk_1.8.0_151'}
rootdir: F:\my-video-app
plugins: allure-pytest-2.15.3, anyio-3.7.1, base-url-2.0.0, cov-4.1.0, html-3.2.0, metadata-3.0.0, playwright-0.3.3
collected 46 items
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[\u98ce\u666f-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[!@#$%^&*()-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[' OR '1'='1-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[VALID_ID-200-False] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[99999999-404-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[-1-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[abc-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-\u6b63\u5e38\u6d4b\u8bd5\u8bc4\u8bba-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True--400] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True- -400] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-<script>alert('xss')</script>-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[False-\u7f3a\u5c11\u89c6\u9891ID-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_methods[GET] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_methods[POST] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[99999999-404] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[abc-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[-5-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[1-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[2-5-\u98ce\u666f-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[0-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[-1-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[1-99999--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[abc-xyz--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[-] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[\u6d4b\u8bd5-] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[-\u98ce\u666f] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[\u6d4b\u8bd5-\u98ce\u666f] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search['; DROP TABLE comments;---] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[99999999-404] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[abc-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[-1-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_success PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[GET-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[PUT-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[DELETE-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[GET-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[POST-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[PUT-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[DELETE-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_view_counts_structure PASSED
------------------------------------- generated html file: file:///F:/my-video-app/report.html --------------------------------------
======================================================== 46 passed in 58.23s ========================================================
pytest-html 测试报告截图
pytest-html 测试报告
1.1.3 Allure 生成测试报告
pytest -s -v --alluredir=./allure-results 40_tets.py
#执行测试并生成临时数据
allure serve ./allure-results # 启动 Allure 报告面板
PS F:\my-video-app> pytest -s -v --alluredir=./allure-results 40_tets.py
======================================================== test session starts ========================================================
platform win32 -- Python 3.7.8, pytest-7.4.4, pluggy-1.2.0 -- E:\python\python.exe
cachedir: .pytest_cache
metadata: {'Python': '3.7.8', 'Platform': 'Windows-10-10.0.22621-SP0', 'Packages': {'pytest': '7.4.4', 'pluggy': '1.2.0'}, 'Plugins': {'allure-pytest': '2.15.3', 'anyio': '3.7.1', 'base-url': '2.0.0', 'cov': '4.1.0', 'html': '3.2.0', 'metadata': '3.0.0', 'playwright': '0.3.3'}, 'JAVA_HOME': 'F:\\jdk_1.8.0_151'}
rootdir: F:\my-video-app
plugins: allure-pytest-2.15.3, anyio-3.7.1, base-url-2.0.0, cov-4.1.0, html-3.2.0, metadata-3.0.0, playwright-0.3.3
collected 46 items
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[\u98ce\u666f-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[!@#$%^&*()-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_video_list_and_search[' OR '1'='1-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[VALID_ID-200-False] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[99999999-404-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[-1-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[abc-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_video_details[-400-True] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-\u6b63\u5e38\u6d4b\u8bd5\u8bc4\u8bba-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True--400] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True- -400] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-<script>alert('xss')</script>-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[True-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA-201] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_boundaries[False-\u7f3a\u5c11\u89c6\u9891ID-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_methods[GET] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_methods[POST] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[99999999-404] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[abc-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[-5-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_update_view_exceptions[-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[1-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[2-5-\u98ce\u666f-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[0-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[-1-5--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[1-99999--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_videos_pagination_and_search[abc-xyz--200] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[-] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[\u6d4b\u8bd5-] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[-\u98ce\u666f] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search[\u6d4b\u8bd5-\u98ce\u666f] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_search['; DROP TABLE comments;---] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[99999999-404] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[abc-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_exceptions[-1-400] PASSED
40_tets.py::TestComprehensiveAPIs::test_manage_comments_delete_success PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[GET-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[PUT-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_add_comment_method_not_allowed[DELETE-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[GET-200] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[POST-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[PUT-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_analytics_methods_and_data[DELETE-405] PASSED
40_tets.py::TestComprehensiveAPIs::test_get_view_counts_structure PASSED
======================================================== 46 passed in 52.26s ========================================================
启动 Allure 报告面板
1.2 基于 Playwright 的UI自动化测试
为了模拟真实用户的操作路径,本部分采用当下最先进的现代自动化框架 Playwright,实现前端交互与后端接口拦截的完美融合。
1.2.0 Playwright UI自动化文档
-
test_c_end_user_core_journey(用户核心体验链路)-
业务场景:模拟普通用户从打开首页、搜索视频、点击播放到发表评论的完整旅程。
-
设计 (网络拦截与显式等待):抛弃了不稳定的
time.sleep(),采用 Playwright 的page.expect_response()机制。例如,点击搜索按钮后,脚本会挂起,直到精准拦截到/api/connect_PgSql?search=...接口返回 200 OK,且前端 DOM 树成功渲染出对应的视频卡片后,才进行下一步断言。同时,测试了不刷新页面的情况下,新发表的评论是否通过 DOM 操作成功置顶回显。
-
-
test_b_end_comment_audit_and_cleanup(管理员评论审核与管控链路)-
业务场景:模拟管理员登录后台,检索特定关键字违规评论并批量清理。
-
**设计 **:在删除多条评论时,每次点击删除都会导致 DOM 刷新。此用例通过
while循环,动态检测页面上剩余的“删除”按钮并永远只点击第一个,规避了传统自动化测试中极易触发的“元素陈旧异常”。
-
-
test_b_end_video_crud_lifecycle(管理员视频生命周期闭环: 物理直传与销毁)-
业务场景:完整模拟大文件(视频+封面图)的真实物理上传、跨面板状态同步、二次编辑以及物理文件的销毁。
-
设计 (动态 ID 流转与长耗时接管):
-
超长超时机制:物理上传直传云端非常耗时,代码中专门针对
POST /api/manage-videos接口赋予了 60 秒的超长等待。 -
动态靶向定位:成功上传后,脚本会从后端的 JSON 响应中“拿到”
newVideoId。在随后的修改和删除操作中,不需要遍历表格,而是直接利用 CSS 选择器tr[data-video-id="{newVideoId}"]实施精准靶向打击。 -
UI 遮罩层攻克:精准识别并处理前端 Alert 模态框,防止遮罩层阻断后续的自动化点击。
-
-
-
test_b_end_video_complex_update(管理员复杂更新交互: 标签增删与物理替换)-
业务场景:测试高难度的前端交互组件(如自定义 Tag 输入框)及底层存储文件更替逻辑。
-
核心逻辑:极速构建一个临时测试视频后,模拟精准点击 Tag 的
×按钮删除特定标签,并在自定义输入框中敲击Enter生成新标签。随后,利用set_input_files将底层的封面和视频文件进行强制替换。最后通过接口断言,确认腾讯云 COS 的新文件上传与旧文件删除机制正常触发。
-
1.2.1 接口自动化测试脚本
import pytest
import time
import re
from playwright.sync_api import Page, expect
import urllib.parse
import os
# ==========================================
# ⚙️ 浏览器全局配置:解决没有全屏的问题
# ==========================================
@pytest.fixture(scope="session")
def browser_type_launch_args(browser_type_launch_args):
"""配置浏览器启动参数,使其默认最大化"""
return {
**browser_type_launch_args,
"args": ["--start-maximized"]
}
@pytest.fixture(scope="session")
def browser_context_args(browser_context_args):
"""禁用 Playwright 默认的固定视口,跟随系统窗口大小"""
return {
**browser_context_args,
"no_viewport": True
}
BASE_URL = "https://xxx.cn"
# 第一个函数:用户访问+评论
def test_c_end_user_core_journey(page: Page):
"""
测试套件一:C 端用户核心体验链路 (E2E)
"""
# ---------------------------------------------------------
# 🎬 步骤 1:访问首页,等待视频列表加载与 DOM 渲染
# ---------------------------------------------------------
print("\n[Step 1] 访问首页,等待视频列表加载...")
with page.expect_response("**/api/connect_PgSql*") as response_info:
page.goto(f"{BASE_URL}/index.html")
assert response_info.value.ok
# 严格等待前端 DOM 渲染完成:确保“视频加载中...”消失,且至少一个视频卡片渲染完毕
expect(page.locator("#video-grid p:has-text('视频加载中...')")).not_to_be_visible()
page.wait_for_selector(".video-card", state="visible")
# 【视觉停顿】观赏 (3秒)
page.wait_for_timeout(3000)
# ---------------------------------------------------------
# 🎬 步骤 2:模拟搜索操作,验证列表过滤
# ---------------------------------------------------------
print("[Step 2] 模拟用户输入搜索词并搜索...")
first_video_title = page.locator(".video-card").first.locator("h3").inner_text()
search_term = first_video_title[:2] if len(first_video_title) > 2 else first_video_title
page.locator("#search-input").fill(search_term)
with page.expect_response(re.compile(r".*/api/connect_PgSql\?search=.*")) as search_resp:
page.locator("#search-form button").click()
assert search_resp.value.ok
# 严格等待前端 DOM 刷新:标题变更,且新的结果卡片渲染出来
expect(page.locator("#main-title")).to_have_text(f'搜索: "{search_term}"')
page.wait_for_selector(".video-card", state="visible")
target_video_card = page.locator(".video-card").first
target_video_title = target_video_card.locator("h3").inner_text()
page.wait_for_timeout(1500)
# ---------------------------------------------------------
# 🎬 步骤 3:点击视频卡片,验证底层 update_view 触发及跳转
# ---------------------------------------------------------
print("[Step 3] 点击视频,验证跳转及详情加载...")
with page.expect_response("**/api/get_video_details*") as details_resp:
target_video_card.click()
assert details_resp.value.ok
expect(page).to_have_url(re.compile(r".*player\.html\?id=\d+"))
# ---------------------------------------------------------
# 🎬 步骤 4:播放页渲染断言
# ---------------------------------------------------------
print("[Step 4] 严格验证播放页 DOM 渲染...")
# 必须等待标题不再是占位符“加载中...”
expect(page.locator("#video-title")).not_to_have_text("加载中...")
expect(page.locator("#video-title")).to_have_text(target_video_title)
expect(page.locator("#video-wrapper video")).to_be_visible()
page.wait_for_timeout(1500)
# ---------------------------------------------------------
# 🎬 步骤 5:交互评论与无刷新 DOM 回显验证
# ---------------------------------------------------------
print("[Step 5] 模拟用户输入带时间戳的评论并发布...")
timestamp = int(time.time())
test_comment = f"【Playwright自动化测试】零延迟极速体验!时间戳:{timestamp}"
page.locator("#comment-input").fill(test_comment)
with page.expect_response("**/api/add_comment") as comment_resp:
page.locator("#comment-submit-btn").click()
assert comment_resp.value.status == 201
# 等待前端 JS 把新评论 append 到列表顶部
first_comment_locator = page.locator(".comment-content").first
expect(first_comment_locator).to_have_text(test_comment)
# 最后停留3秒,让你看清楚发布的评论再关闭浏览器
page.wait_for_timeout(3000)
print("✅ 用户核心体验链路测试通过,丝滑且清晰!")
# 第二个函数:搜索对应评论 + 循环删除 + 彻底清理
def test_b_end_comment_audit_and_cleanup(page: Page):
"""
测试套件三:B 端评论审核与管控链路
覆盖场景:进入管理后台 -> 切换评论 Tab -> 搜索包含“时间戳”的测试评论 -> 循环精准删除 -> 验证数据彻底清空
"""
# ---------------------------------------------------------
# 🎬 步骤 1:访问管理后台并切换到评论 Tab
# ---------------------------------------------------------
print("\n[Step 1] 访问管理后台,切换至评论管理...")
page.goto(f"{BASE_URL}/management.html")
# 点击侧边栏的“评论管理” Tab,并监听随之触发的数据请求
with page.expect_response("**/api/manage-comments*") as init_resp:
page.locator('.nav-link[data-tab="comments"]').click()
assert init_resp.value.ok
# 等待评论面板激活,且状态提示消失
expect(page.locator("#comments-panel")).to_have_class(re.compile(r"active"))
page.wait_for_timeout(1000) # 视觉停顿
# ---------------------------------------------------------
# 🎬 步骤 2:多维搜索特定的测试评论
# ---------------------------------------------------------
print("[Step 2] 搜索带有“时间戳”关键字的测试评论...")
search_keyword = "时间戳"
page.locator("#comments-search-content").fill(search_keyword)
# 将 "时间戳" 转换为 "%E6%97%B6%E9%97%B4%E6%88%B3"
encoded_keyword = urllib.parse.quote(search_keyword)
with page.expect_response(re.compile(rf".*/api/manage-comments.*commentSearch={encoded_keyword}.*")) as search_resp:
page.locator("#comments-search-btn").click()
assert search_resp.value.ok
page.wait_for_timeout(1000) # 视觉停顿
# ---------------------------------------------------------
# 🎬 步骤 3:循环清理所有匹配的测试数据 (规避 Stale Element)
# ---------------------------------------------------------
print("[Step 3] 开始循环清理测试脏数据...")
delete_buttons = page.locator("#comments-table-body .btn-action-delete")
# 只要列表里还有删除按钮,就一直删
while delete_buttons.count() > 0:
count_before = delete_buttons.count()
print(f" -> 当前列表还有 {count_before} 条测试评论,准备删除第一条...")
# 1. 点击第一个删除按钮
delete_buttons.first.click()
# 2. 等待确认弹窗完全显示
expect(page.locator("#delete-comment-modal")).to_have_class(re.compile(r"active"))
# 3. 点击确认删除,并严格监听 DELETE 和自动触发的 GET 刷新接口
with page.expect_response("**/api/manage-comments*commentId=*") as del_resp:
page.locator("#delete-comment-confirm-btn").click()
# 断言删除接口成功
assert del_resp.value.request.method == "DELETE"
assert del_resp.value.ok
# 4. 隐式等待:Playwright 会在下一次 while 循环检查 count() 时自动等待 DOM 稳定
# 为了肉眼观赏,稍微停顿一下
page.wait_for_timeout(800)
print("✅ 所有“时间戳”相关的测试评论已清理完毕!")
# ---------------------------------------------------------
# 🎬 步骤 4:闭环断言
# ---------------------------------------------------------
print("[Step 4] 终极断言:验证表格内已无相关数据...")
expect(page.locator("#comments-table-body")).to_contain_text("未找到评论。")
page.wait_for_timeout(1500)
print("🎉 B端评论审核与管控链路测试通过!数据库干干净净!")
# ==========================================
# ⚙️ 真实的物理测试
# ==========================================
COVER_PATH = r"D:\temp_video\covers\17_beautifulword.jpg"
VIDEO_PATH = r"D:\temp_video\17_beautifulword.mp4"
# 第三个函数:视频 CRUD 全生命周期管理:从物理上传到跨面板修改再到彻底删除,完整覆盖 B 端视频管理的核心链路
def test_b_end_video_crud_lifecycle(page: Page):
"""
测试套件二:B 端视频全生命周期管理 (CRUD 闭环)
覆盖场景:物理大文件上传 -> 跨面板动态 ID 追踪 -> 信息精准修改 -> 数据与文件的彻底物理销毁
"""
# 前置检查:确保本地测试物料真的存在,防止因为路径写错直接导致用例失败
assert os.path.exists(COVER_PATH), f"❌ 找不到测试封面图: {COVER_PATH}"
assert os.path.exists(VIDEO_PATH), f"❌ 找不到测试视频: {VIDEO_PATH}"
# ---------------------------------------------------------
# 🎬 阶段一 & 二:模拟真实物理上传 (Create)
# ---------------------------------------------------------
print("\n[Step 1] 访问管理后台,准备执行物理文件上传...")
page.goto(f"{BASE_URL}/management.html")
# 填写基础文本信息
page.locator("#upload-title").fill("你是不是很久没见过这个美丽的世界了")
page.locator("#upload-description").fill("光与影的褶皱里,藏着世界未说出口的温柔。")
page.wait_for_timeout(500)
# 攻克自定义标签组件:填入文本后敲击 Enter 键生成 Tag
page.locator("#upload-tag-container .tag-input-field").fill("风景")
page.locator("#upload-tag-container .tag-input-field").press("Enter")
page.wait_for_timeout(1500)
# 断言标签组件是否成功生成了带有 "风景" 文本的 chip 元素
expect(page.locator("#upload-tag-container .tag-chip")).to_contain_text("风景")
# 挂载真实的物理文件到底层 <input type="file"> 中
page.locator("#upload-cover-input").set_input_files(COVER_PATH)
page.locator("#upload-video-input").set_input_files(VIDEO_PATH)
page.wait_for_timeout(2000)
print("[Step 2] 提交上传,正在等待 COS 与数据库响应 (耗时较长,请耐心等待)...")
# 核心难点:真实的上传非常耗时,必须给这个接口极其宽裕的超时时间 (这里设为 60000 毫秒 / 60 秒)
with page.expect_response("**/api/manage-videos", timeout=60000) as upload_resp:
page.locator("#upload-submit-btn").click()
# 提取极其宝贵的动态数据:从后端响应中精准捕获新生成的 videoId
assert upload_resp.value.ok, "视频上传接口报错!"
upload_data = upload_resp.value.json()
new_video_id = upload_data.get("video", {}).get("newVideoId")
assert new_video_id is not None, "未能从上传响应中提取到 newVideoId!"
print(f"✅ 上传成功!成功捕获新视频 ID: {new_video_id}")
page.wait_for_timeout(1000)
# 处理 UI 阻塞:点击上传成功后的全局 Alert 确认弹窗
expect(page.locator("#alert-modal")).to_have_class(re.compile(r"active"))
page.locator("#alert-modal-ok-btn").click()
page.wait_for_timeout(1000)
# ---------------------------------------------------------
# 🎬 阶段三:跨面板状态同步与精准修改 (Read & Update)
# ---------------------------------------------------------
print("[Step 3] 切换至视频管理面板,执行数据修改...")
# 切换 Tab 并等待列表接口返回
with page.expect_response("**/api/manage-videos*page=1*") as list_resp:
page.locator('.nav-link[data-tab="management"]').click()
assert list_resp.value.ok
# 使用上一步拿到的动态 ID,在茫茫表格中精准锁定我们要操作的那一行
target_row = page.locator(f'tr[data-video-id="{new_video_id}"]')
# 断言刚刚上传的数据已经正确渲染在表格中了
expect(target_row).to_be_visible()
page.wait_for_timeout(1000)
# 点击那一行的“编辑”按钮
target_row.locator(".btn-action-edit").click()
expect(page.locator("#edit-modal")).to_have_class(re.compile(r"active"))
page.wait_for_timeout(1000)
# 在原标题基础上加上修改标识
new_title = "你是不是很久没见过这个美丽的世界了 - [UI自动化修改]"
page.locator("#edit-title").fill(new_title)
page.wait_for_timeout(1000)
# 再追加一个测试标签
page.locator("#edit-tag-container .tag-input-field").fill("自动化测试")
page.locator("#edit-tag-container .tag-input-field").press("Enter")
page.wait_for_timeout(1000)
print(" -> 保存修改信息...")
with page.expect_response("**/api/manage-videos", timeout=30000) as edit_resp:
page.locator("#edit-save-btn").click()
assert edit_resp.value.request.method == "PUT", "调用的不是 PUT 更新接口!"
assert edit_resp.value.ok
# 模态框应该自动关闭,且表格里的标题应该发生改变
expect(page.locator("#edit-modal")).not_to_have_class(re.compile(r"active"))
expect(target_row).to_contain_text("[UI自动化修改]")
print("✅ 视频信息修改成功,UI 状态已同步更新!")
page.wait_for_timeout(2000)
# ---------------------------------------------------------
# 🎬 阶段四:物理与逻辑的彻底销毁 (Delete & Teardown)
# ---------------------------------------------------------
print("[Step 4] 执行测试扫尾:彻底删除该视频与关联文件...")
# 依然精准打击这一行,点击删除
target_row.locator(".btn-action-delete").click()
expect(page.locator("#delete-confirm-modal")).to_have_class(re.compile(r"active"))
page.wait_for_timeout(1000)
# 确认删除并拦截 DELETE 请求
with page.expect_response(f"**/api/manage-videos?videoId={new_video_id}") as del_resp:
page.locator("#delete-confirm-btn").click()
assert del_resp.value.request.method == "DELETE"
assert del_resp.value.ok
# 终极断言:这个 DOM 节点已经彻底消失,不仅保证了 UI 的正确性,也确保了下次跑脚本时数据库是干净的
expect(target_row).not_to_be_visible()
page.wait_for_timeout(1000) # 视觉停顿
print(f"🎉 视频 CRUD 闭环测试完美通过!测试数据 (ID:{new_video_id}) 已安全销毁。")
# 你提供的新物理测试物料
NEW_COVER_PATH = r"D:\temp_video\covers\16_faluo.jpg"
NEW_VIDEO_PATH = r"D:\temp_video\16_faluoqundao.mp4"
#第四个函数:上传后修改:删除旧标签、增加新标签、替换封面和视频源文件,最后再删除这个视频,保持环境干净
def test_b_end_video_complex_update(page: Page):
"""
测试复杂修改:删除标签、增加标签、替换封面和视频源文件
"""
# 确保新文件存在
assert os.path.exists(NEW_COVER_PATH), f"❌ 找不到新封面图: {NEW_COVER_PATH}"
assert os.path.exists(NEW_VIDEO_PATH), f"❌ 找不到新视频: {NEW_VIDEO_PATH}"
print("\n[Step 1] 极速造数据:先上传一个带初始标签的临时视频...")
page.goto(f"{BASE_URL}/management.html")
# 造一个基础视频
page.locator("#upload-title").fill("临时视频-准备被修改")
page.locator("#upload-description").fill("临时视频-描述")
page.locator("#upload-tag-container .tag-input-field").fill("旧标签待删除")
page.locator("#upload-tag-container .tag-input-field").press("Enter")
# 使用你之前的封面和视频作为初始文件
page.locator("#upload-cover-input").set_input_files(COVER_PATH)
page.locator("#upload-video-input").set_input_files(VIDEO_PATH)
page.wait_for_timeout(1000)
with page.expect_response("**/api/manage-videos", timeout=60000) as upload_resp:
page.locator("#upload-submit-btn").click()
page.wait_for_timeout(1000)
new_video_id = upload_resp.value.json().get("video", {}).get("newVideoId")
page.locator("#alert-modal-ok-btn").click()
page.wait_for_timeout(1000)
# ---------------------------------------------------------
# 🎬 核心测试开始:复杂的修改交互
# ---------------------------------------------------------
print("[Step 2] 切换至管理面板,打开编辑模态框...")
with page.expect_response("**/api/manage-videos*page=1*"):
page.locator('.nav-link[data-tab="management"]').click()
page.wait_for_timeout(1500)
target_row = page.locator(f'tr[data-video-id="{new_video_id}"]')
target_row.locator(".btn-action-edit").click()
expect(page.locator("#edit-modal")).to_have_class(re.compile(r"active"))
page.wait_for_timeout(1000)
print("[Step 3] 执行复杂修改:操作标签与替换物理文件...")
# 1. 删除旧标签:定位到编辑框里的第一个标签,并点击它的 '×' 按钮
first_tag_remove_btn = page.locator("#edit-tag-container .tag-chip").first.locator(".tag-remove-btn")
first_tag_remove_btn.click()
page.wait_for_timeout(1000)
# 2. 增加新标签
page.locator("#edit-tag-container .tag-input-field").fill("法罗群岛")
page.locator("#edit-tag-container .tag-input-field").press("Enter")
# 3. 挂载新的封面和视频文件
page.locator("#edit-cover-input").set_input_files(NEW_COVER_PATH)
page.locator("#edit-video-input").set_input_files(NEW_VIDEO_PATH)
page.wait_for_timeout(1000)
print(" -> 保存复杂修改,等待 COS 上传新文件并回滚旧文件...")
# 拦截 PUT 请求,因为涉及新文件上传到腾讯云,给足超时时间
with page.expect_response("**/api/manage-videos", timeout=60000) as edit_resp:
page.locator("#edit-save-btn").click()
assert edit_resp.value.ok, "更新接口报错!"
# 验证 UI 上的标签是否更新(“旧标签待删除”应消失,“法罗群岛”应出现)
expect(target_row.locator(".tag-chip-container")).not_to_contain_text("旧标签待删除")
expect(target_row.locator(".tag-chip-container")).to_contain_text("法罗群岛")
print("✅ 复杂修改测试通过:标签增删及大文件替换均成功!")
# ---------------------------------------------------------
# 🎬 扫尾清理
# ---------------------------------------------------------
print("[Step 4] 测试扫尾:删除该视频...")
target_row.locator(".btn-action-delete").click()
page.wait_for_timeout(1000)
with page.expect_response(f"**/api/manage-videos?videoId={new_video_id}"):
page.locator("#delete-confirm-btn").click()
page.wait_for_timeout(1000)
print("✅ 测试数据彻底清理完毕!")
# pytest test_c_end_journey.py --headed -s
# pytest test_c_end_journey.py --headed -s --alluredir=./result_1
# allure serve ./result_1 # 启动 Allure 报告面板
# pytest test_c_end_journey.py -k "audit_and_cleanup" --headed -s
# pytest test_c_end_journey.py -k "complex_update" --headed -s
1.2.2 Allure 生成测试报告
PS F:\my-video-app> pytest test_c_end_journey.py --headed -s --alluredir=./result_1
=============================================================== test session starts ================================================================
platform win32 -- Python 3.7.8, pytest-7.4.4, pluggy-1.2.0
rootdir: F:\my-video-app
plugins: allure-pytest-2.15.3, anyio-3.7.1, base-url-2.0.0, html-3.2.0, metadata-3.0.0, playwright-0.3.3
collected 4 items
test_c_end_journey.py
[Step 1] 访问首页,等待视频列表加载...
[Step 2] 模拟用户输入搜索词并搜索...
[Step 3] 点击视频,验证跳转及详情加载...
[Step 4] 严格验证播放页 DOM 渲染...
[Step 5] 模拟用户输入带时间戳的评论并发布...
✅ 用户核心体验链路测试通过,丝滑且清晰!
.
[Step 1] 访问管理后台,切换至评论管理...
[Step 2] 搜索带有“时间戳”关键字的测试评论...
[Step 3] 开始循环清理测试脏数据...
-> 当前列表还有 1 条测试评论,准备删除第一条...
✅ 所有“时间戳”相关的测试评论已清理完毕!
[Step 4] 终极断言:验证表格内已无相关数据...
🎉 B端评论审核与管控链路测试通过!数据库干干净净!
.
[Step 1] 访问管理后台,准备执行物理文件上传...
[Step 2] 提交上传,正在等待 COS 与数据库响应 (耗时较长,请耐心等待)...
✅ 上传成功!成功捕获新视频 ID: 32
[Step 3] 切换至视频管理面板,执行数据修改...
-> 保存修改信息...
✅ 视频信息修改成功,UI 状态已同步更新!
[Step 4] 执行测试扫尾:彻底删除该视频与关联文件...
🎉 视频 CRUD 闭环测试完美通过!测试数据 (ID:32) 已安全销毁。
.
[Step 1] 极速造数据:先上传一个带初始标签的临时视频...
[Step 2] 切换至管理面板,打开编辑模态框...
[Step 3] 执行复杂修改:操作标签与替换物理文件...
-> 保存复杂修改,等待 COS 上传新文件并回滚旧文件...
✅ 复杂修改测试通过:标签增删及大文件替换均成功!
[Step 4] 测试扫尾:删除该视频...
✅ 测试数据彻底清理完毕!
.
=========================================================== 4 passed in 85.86s (0:01:25) ===========================================================
启动 Allure 报告面板
2 测试用例
2.1 主页视频列表展示与搜索
前置条件
-
确保本地
vercel dev服务正常运行,或使用已配置好的 Vercel 线上域名。 -
PostgreSQL 数据库
videos表 、tags表 、video_tags表与comments表连接正常。 -
腾讯云 COS 状态正常。
测试步骤
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 浏览器访问 /index.html | 页面正常加载,顶部展示导航栏与搜索框。 |
| 2 | 查看核心主区域 | 成功渲染「精选视频」列表,每个视频卡片均包含:封面图、视频标题、上传日期、以及动态拉取的真实播放量。 |
| 3 | 在顶部搜索框输入现有视频的标题或标签关键字,点击搜索图标 | 页面不发生全页刷新,标题变为 搜索: "关键字",视频网格中仅展示标题或标签匹配的视频卡片。 |
| 4 | 点击任意视频卡片 | 触发底层播放量统计接口,并成功携带 id 参数跳转至 player.html。 |
预期结果
| 模块 | 具体成果详情 |
|---|---|
| 数据库 (videos 表) | 对应视频的views_count + 1 。 |
2.2 视频播放防盗链与评论交互
前置条件
- 访问
/index.html页面
测试步骤
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 从主页点击视频,进入 /player.html?id=xxx | 页面正确获取视频详情,原生的播放器挂载成功并开始自动播放。 |
| 2 | 查看控制台网络请求 | 数据库接口返回的视频播放地址 signed_play_url 包含了 MD5 动态生成的 sign 签名和 t(时间戳),有效验证了 CDN 防盗链机制。 |
| 3 | 在下方评论区输入一条带时间戳的测试评论并点击“发布评论” | 评论框清空,“发布评论”按钮解除禁用状态。 |
| 4 | 观察评论列表区 | 无需刷新整个页面,新发布的评论直接显示在评论列表最顶部。 |
预期结果
| 模块 | 具体成果详情 |
|---|---|
| 数据库 (videos 表) | 对应视频id的views_count + 1 。 |
| 数据库 (comments 表) | 新增 1 条数据:comment_id 递增;video_id 为该视频 id;content 初始化为带时间戳的测试评论;created_at 为当前时间 |
2.3 管理员页面:视频上传(Create)
前置条件
-
访问
/management.html。 -
本地准备好符合格式规范的测试物料(如
.jpg封面,.mp4视频)。
测试步骤
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 点击左侧导航栏的「视频上传」 | 右侧展示上传表单,包含标题、描述、标签输入框及文件上传区域。 |
| 2 | 填写标题与描述,在标签框输入 童年、生活 并敲击回车 | 标签框内正确生成带有 × 按钮的蓝色交互式 Tag Chip 元素。 |
| 3 | 分别选择本地的封面图与视频文件 | 封面上传区域成功渲染本地图片的预览缩略图。 |
| 4 | 点击「提交上传」按钮 | 按钮变为“上传中…”,显示上传进度条。向腾讯云请求临时 STS 密钥并直传文件。 |
| 5 | 等待进度条到达 100% | 弹出「上传成功」全局确认框,表单自动重置清空。 |
预期结果
| 模块 | 具体成果详情 |
|---|---|
| 数据库 (videos 表) | 新增 1 条数据:id 递增;cover_key 记录为 covers/{文件名};video_key 记录为 videos/{文件名};views_count 初始化为 0;upload_date 为当前上传时间。 |
| 数据库 (tags 表) | 若标签不存在则新建并生成 tag_id,若存在则复用现有 tag_id。保证全库标签唯一。 |
| 数据库 (video_tags 表) | 新增映射记录,完成当前 video_id 与对应 tag_id 的多对多绑定。 |
| 云存储 (腾讯云 COS) | video-321 存储桶内,covers/ 和 videos/ 目录下成功生成对应的物理文件。 |
2.4 管理员页面:视频信息修改(Update)
前置条件
- 访问
/management.html,进入「视频管理」。
测试步骤
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 在列表中找到目标视频,点击操作列的「编辑」按钮 | 弹出 #edit-modal 编辑模态框,并自动回显该视频的所有原始信息(标题、描述、旧标签、旧封面预览图)。 |
| 2 | 修改视频标题,删除一个旧标签,新增一个测试标签 | 文本与标签组件 UI 状态实时同步更新。 |
| 3 | 点击「上传新封面」「上传新视频」选择一个全新的封面和视频文件 | 封面上传区域成功渲染本地图片的预览缩略图,触发文件替换逻辑的预备状态。 |
| 4 | 点击模态框底部的「保存更改」 | 后台开始上传新文件至 COS,写入数据库,并触发旧文件的清理机制。 |
| 5 | 等待请求完成 | 模态框自动关闭,页面顶部提示绿色「更新成功」,表格内当前行的标题与标签数据实时刷新。 |
预期结果
| 模块 | 具体成果详情 |
|---|---|
| 数据库 (videos 表) | 对应记录的标题、描述更新;cover_key 记录变为 covers/{新文件名};video_key 记录变为 videos/{新文件名};upload_date 被更新为当前修改时间 。 |
| 数据库 (tags 表) | 若标签不存在则新建并生成 tag_id,若存在则复用现有 tag_id。保证全库标签唯一。 |
| 数据库 (video_tags 表) | 删除旧的映射记录;新增映射记录,完成当前 video_id 与对应 tag_id 的多对多绑定。 |
| 云存储 (腾讯云 COS) | video-321 存储桶内,covers/ 和 videos/ 目录下成功生成对应的物理文件。;同时旧的物理文件被彻底删除,防止云端空间浪费。 |
2.5 管理员页面:视频删除(Delete)
前置条件
- 访问
/management.html,进入「视频管理」。
测试步骤
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 在「视频管理」中,搜索待删除的视频标题 | 列表过滤,仅展示匹配项。 |
| 2 | 点击目标视频操作列的「删除」按钮 | 弹出 #delete-confirm-modal 红色预警弹窗,提示操作不可逆。 |
| 3 | 点击「确认删除」按钮 | 弹窗关闭,列表展示「删除中…」并随后提示成功。 |
| 4 | 等待请求完成 | 表格内无当前行的标题与标签数据。 |
预期结果
| 模块 | 具体成果详情 |
|---|---|
| 数据库 (videos 表) | videos 表对应该视频id的所有信息被删除。 |
| 数据库 (video_tags 表) | 视频id对应 video_tags 表中的所有标签关联记录被删除。 |
| 数据库 (comments 表) | 视频id该对应 comments 表下的所有评论数据被被删除。 |
| 云存储 (腾讯云 COS) | 腾讯云 COS 中关联的封面图片和视频文件被双双抹除。 |
2.6 管理员页面:评论审核与清理
前置条件
- 访问
/management.html
测试步骤
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 切换「评论管理」 | 渲染全站评论表格,包含评论内容、所属视频标题、发表时间。 |
| 2 | 使用双重条件检索:在搜索框分别填入特定的“评论关键字”和“视频标题” | 底层触发带有联合查询的 SQL,表格精准筛选出匹配的评论。 |
| 3 | 点击评论的「删除」按钮,在弹窗中确认 | 接口调用成功后,该评论从列表中消失。对应播放页不再显示该条评论。 |
预期结果
| 模块 | 具体成果详情 |
|---|---|
| 数据库 (comments 表) | 对应的评论id数据被被删除。 |
2.7 管理员页面:统计分析大屏
前置条件
- 访问
/management.html
测试步骤
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 切换至「统计分析」 | 页面出现三个模块的 正在加载... 提示。 |
| 2 | 观察数据渲染完毕后的展示 | 三个卡片分别正确渲染: 1. 播放量 Top 5 视频排行。 2. 价值 Top 5 标签。 3. 热门 Top 5 标签。 |
3 解决BUG
- 执行测试用例15的时候,评论页面插入一个2000个字的评论,导致播放页面被拉长,比例不协调。在播放页面和后台页面针对50字以上的评论加展开收回功能,保证了观看体验;
- manage-video.js的POST提交请求,返回状态码修改为201,而不是200;
- manage-comment.js的99行代码修改为:if (result.length === 0),判断删除后是否有返回,判断长度用length而不是rowCount;
- 用例19针对不存在的id返回404,用例20-22针对负数、字母和空的id返回400。优化update_view.js逻辑;
- 用例8-10针对负数、字母和空的id返回400(之前未做判断,导致返回500,不合理),优化get_video_details.js逻辑;
- 用例35-37针对负数、字母和空的评论id返回400。优化manage-comments.js的DELETE逻辑;
- 限制page和limit必须为正数,且limit被限制最大为10(避免一次加载太多卡顿)。优化manage-videos.js的GET逻辑;














2524

被折叠的 条评论
为什么被折叠?



