ccf接口测试实战
一、比赛题目
练习赛-测试人网站搜索接口自动化测试 - 霍格沃兹测试开发学社 / 霍格沃兹测试学院教务处 - 爱测-测试人社区
二、分析题目
一、题目解析
✅ 核心任务:
使用 Python 编写接口自动化测试脚本,对
https://ceshiren.com/search
接口进行全面测试,并打包成.zip
文件提交。
🔍 题目关键信息提取:
项目 | 内容 |
---|---|
被测接口 | GET https://ceshiren.com/search |
请求方式 | GET |
必须 header | "Accept": "application/json" |
关键参数 | q (搜索关键词)、limit (返回条数)、term (用户搜索字段) |
测试重点 | 搜索功能的 正确性 和 稳定性 |
筛选条件 | 支持分类、标签、发帖人等(但在当前接口中未体现,暂不处理) |
提交要求 | 只接受 .zip 压缩包,语言限 Python 或 Java |
评分标准 | 用例覆盖、参数化、断言、封装、PO模式、token复用、代码规范 |
🧩 二、你应该怎么做?(分步指导)
✅ 第一步:理解接口行为
访问这个页面: 👉 https://ceshiren.com/search?expanded=true
这是「高级搜索」界面,你可以输入关键词、选择分类等。
但注意:接口层面目前只提供了 q
参数作为主要搜索字段。
你可以手动测试几个请求:
# 示例请求
GET https://ceshiren.com/search?q=pytest&limit=10
返回的是 JSON 格式数据,包含:
posts
: 匹配的帖子列表(搜索结果应为空列表,表示没有找到相关内容,不传page,post不为空)topics
: 匹配的话题groups
: 匹配的群组users
: 匹配的用户grouped_search_result
: 聚合结果(不传参数时,缺少q,搜索关键词为空,响应不应包含 grouped_search_resul
)
✅ 第二步:设计测试用例(等价类 + 边界值)
📋 测试用例表
编号 | 用例标题 | 请求参数 | 预期结果 | 断言逻辑 | 测试类型 |
---|---|---|---|---|---|
TC01 | 使用有效关键词能返回非空搜索结果 | q=pytest , page=1 <br>(其他关键词类似) | 返回包含 posts 的 JSON 数据,且结果数量 > 0 | 1. 响应状态码为 200(由 2. 响应中包含 | 正向 |
TC02 | 搜索关键词为空字符串时不应返回聚合结果 | q="" , page=1 | 响应中 grouped_search_result 字段为 null 或不存在 | r.json().get("grouped_search_result") is None | 负向 / 边界 |
TC03 | 搜索无匹配内容的关键词返回空结果列表 | q="oooooooooo" , page=1 | posts 字段存在且为空数组 [] | r.json().get("posts") == [] | 负向 |
TC04 | 不传 q 参数时不应返回聚合搜索结果 | params={"page":1} (无 q ) | grouped_search_result 字段为 null 或不存在 | r.json().get("grouped_search_result") is None | 负向 / 缺省校验 |
TC05 | 不传 page 参数时默认返回第一页结果 | q="测试用例" (无 page ) | 成功返回搜索结果,posts 非空 | len(r.json().get("posts")) != 0 | 正向 / 默认值测试 |
TC06 | 完全不传任何参数时不应触发有效搜索 | 无任何参数 | grouped_search_result 字段为 null 或不存在 | r.json().get("grouped_search_result") is None | 负向 / 边界 |
✅ 第三步:满足评分标准(非常重要!)
这是你拿高分的关键,必须逐项满足:
评分项 | 如何实现?✅ |
---|---|
1. 业务流程完整 | 实现“搜索 → 获取结果 → 断言”的完整流程 |
2. 用例参数化 | 使用 @pytest.mark.parametrize |
3. 场景覆盖全面 | 包含正向、负向、边界、异常场景(见上表) |
4. 代码规范(PEP8) | 使用驼峰/下划线命名、空行、注释、函数分离 |
5. 是否使用 PO 模式 | ✅ 必须使用 Page Object 模式封装! |
6. 是否有断言 | 每个测试都有 assert 判断结果 |
7. token 是否复用 | 当前接口无需登录 → 无 token → 可忽略 |
8. 代码是否封装 | 抽取公共方法(如请求封装、base_url) |
🏗️ 三、推荐架构:使用 PO(Page Object)设计模式
虽然这是接口测试,但也可以用 API 层的 PO 模式 来组织代码。
📁 项目结构建议:
三、代码
1.线性版本(简化版)
import pytest
import requestsclass TestCeshirenSearch:def setup_class(self):self.base_url = "https://ceshiren.com"# 搜索接口 urlself.search_url = f"{self.base_url}/search"# 正向用例@pytest.mark.parametrize("search_key",["pytest","面试题","a","appium desktop连接真机,start session,出现报错,手机上appium setting打开闪退,但是进程显示是进行中。报错内容:An unknown server-side error occurred while processing the command. Original error: Could not find a connected Android device in 20364ms.",])def test_search(self, search_key):params = {"q": search_key,"page": 1}headers = {"Accept": "application/json"}r = requests.request(method="GET", url=self.search_url, params=params, headers=headers)print(r.text)results = len(r.json().get("posts"))print(f"响应结果中 posts 结果数量为 {results}")assert results != 0# 搜索关键词为空def test_search_none(self):params = {"q": "","page": 1}headers = {"Accept": "application/json"}r = requests.request(method="GET", url=self.search_url, params=params, headers=headers)print(r.text)assert r.json().get("grouped_search_result") == None# 搜索结果为空def test_search_no_result(self):params = {"q": "ooooooooooo","page": 1}headers = {"Accept": "application/json"}r = requests.request(method="GET", url=self.search_url, params=params, headers=headers)print(r.text)assert r.json().get("posts") == []# 缺少请求参数 qdef test_search_noq(self):params = {"page": 1}headers = {"Accept": "application/json"}r = requests.request(method="GET", url=self.search_url, params=params, headers=headers)print(r.text)assert r.json().get("grouped_search_result") == None# 缺少请求参数 pagedef test_search_nopage(self):params = {"q": "测试用例"}headers = {"Accept": "application/json"}r = requests.request(method="GET", url=self.search_url, params=params, headers=headers)print(r.text)results = len(r.json().get("posts"))print(f"响应结果中 posts 结果数量为 {results}")assert results != 0# 不传请求参数def test_search_noparams(self):headers = {"Accept": "application/json"}r = requests.request(method="GET", url=self.search_url, headers=headers)print(r.text)assert r.json().get("grouped_search_result") == None
✅ 整体优点总结
优点 | 说明 |
---|---|
🟢 结构清晰 | 类封装 + setup_class 初始化 |
🟢 数据驱动 | @pytest.mark.parametrize 提高测试覆盖率 |
🟢 覆盖全面 | 正向、边界、异常、缺失参数等场景 |
🟢 断言合理 | 根据不同场景设置不同预期结果 |
🟢 日志输出 | print(r.text) 便于调试(生产环境建议改为 logging) |
⚠️ 潜在问题与改进建议
1. ❌ 缺少异常处理(健壮性不足)
当前代码未捕获网络异常或 JSON 解析错误,可能导致测试崩溃。
✅ 改进建议:
import requests
from requests.exceptions import RequestException
import jsontry:r = requests.get(...)r.raise_for_status() # 检查 HTTP 状态码json_data = r.json()
except RequestException as e:pytest.fail(f"请求失败: {e}")
except json.JSONDecodeError:pytest.fail("响应不是合法的 JSON")
2. ❌ 断言不够精确(部分场景)
assert r.json().get("grouped_search_result") == None
更推荐使用 is None
判断:
assert r.json().get("grouped_search_result") is None
.get()
返回None
是明确的空值,应使用is
而非==
。
3. ❌ 缺少对 HTTP 状态码的校验
接口可能返回 404
或 500
,但代码只解析 JSON,容易误判。
assert r.status_code == 200
4. ❌ print(r.text)
不适合 CI/CD 环境
在自动化流水线中,print
输出不易管理。
import logging
logging.basicConfig(level=logging.INFO)
logging.info(r.text)
或者使用 pytest 的 -s
结合 logging
。
5. ✅ 可增加更多边界测试(建议扩展)
目前覆盖不错,但仍可补充:
场景 | 建议 |
---|---|
特殊字符搜索 | q="!@#$%^&*" |
空格开头/结尾 | q=" pytest " |
超长关键词 | >1000 字符 |
SQL注入/XSS尝试 | 如 q="<script>alert(1)</script>" (验证前端过滤) |
大小写敏感性 | "Pytest" vs "pytest" |
6. ✅ 可引入 fixture 优化重复代码
当前每个方法都重复定义 headers
和 requests.get
。
@pytest.fixture
def client(self):session = requests.Session()session.headers.update({"Accept": "application/json"})return sessiondef test_search(self, client, search_key):params = {"q": search_key, "page": 1}r = client.get(self.search_url, params=params)...
7. ✅ 建议添加接口响应时间监控(性能维度)
assert r.elapsed.total_seconds() < 2 # 响应时间小于 2 秒
优化版本
import pytest
import requests
import timeclass TestCeshirenSearch:def setup_class(self):self.base_url = "https://ceshiren.com"self.search_url = self.base_url + "/search"@pytest.fixturedef client(self):"""封装请求客户端,复用 headers 和 session"""session = requests.Session()session.headers.update({"Accept": "application/json"})return session@pytest.mark.parametrize("search_key",["pytest","面试题","a","appium desktop连接真机,start session,出现报错,手机上appium setting打开闪退",])def test_search(self, search_key, client):params = {"q": search_key, "page": 1}# 添加延迟,避免触发限流time.sleep(1)# 发送请求r = client.get(self.search_url, params=params, timeout=10)# 新增:显式断言状态码为 200assert r.status_code == 200, f"HTTP 状态码错误: {r.status_code}"# 或者也可以保留 r.raise_for_status(),但你明确要求用 assert,所以用 assertjson_data = r.json()assert "posts" in json_data, "响应缺少 posts 字段"results = len(json_data["posts"])print(f"关键词 '{search_key}' 的搜索结果数: {results}")assert results > 0, f"搜索 '{search_key}' 未返回结果"def test_search_empty_keyword(self, client):"""测试空关键词搜索"""params = {"q": "", "page": 1}r = client.get(self.search_url, params=params, timeout=10)assert r.status_code == 200json_data = r.json()# Discourse 在 q="" 时不会返回 grouped_search_resultassert json_data.get("grouped_search_result") is Nonedef test_search_no_results(self, client):"""测试无结果的关键词"""params = {"q": "nonexistentkeyword12345", "page": 1}r = client.get(self.search_url, params=params, timeout=10)assert r.status_code == 200json_data = r.json()assert len(json_data["posts"]) == 0
2.po设计模式
预告