ccf接口测试实战

发布于:2025-08-03 ⋅ 阅读:(11) ⋅ 点赞:(0)

一、比赛题目

练习赛-测试人网站搜索接口自动化测试 - 霍格沃兹测试开发学社 / 霍格沃兹测试学院教务处 - 爱测-测试人社区

二、分析题目

一、题目解析

✅ 核心任务:

使用 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=pytestpage=1<br>(其他关键词类似) 返回包含 posts 的 JSON 数据,且结果数量 > 0

1. 响应状态码为 200(由 r.raise_for_status() 保证)

2. 响应中包含 "posts" 字段3. len(posts) > 0

正向
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 requests


class TestCeshirenSearch:

    def setup_class(self):
        self.base_url = "https://ceshiren.com"
        # 搜索接口 url
        self.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") == []

    # 缺少请求参数 q
    def 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

    # 缺少请求参数 page
    def 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 json

try:
    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 状态码的校验

接口可能返回 404500,但代码只解析 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 优化重复代码

当前每个方法都重复定义 headersrequests.get

@pytest.fixture
def client(self):
    session = requests.Session()
    session.headers.update({"Accept": "application/json"})
    return session

def 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 time


class TestCeshirenSearch:
    def setup_class(self):
        self.base_url = "https://ceshiren.com"
        self.search_url = self.base_url + "/search"

    @pytest.fixture
    def 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)

        # 新增:显式断言状态码为 200
        assert r.status_code == 200, f"HTTP 状态码错误: {r.status_code}"
        # 或者也可以保留 r.raise_for_status(),但你明确要求用 assert,所以用 assert

        json_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 == 200
        json_data = r.json()
        # Discourse 在 q="" 时不会返回 grouped_search_result
        assert json_data.get("grouped_search_result") is None
        

    def 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 == 200
        json_data = r.json()
        assert len(json_data["posts"]) == 0

2.po设计模式 

预告


网站公告

今日签到

点亮在社区的每一天
去签到