OpenCart 接口自动化测试实战:pytest + Allure + 数据驱动

143 阅读9分钟

——从零搭建三层架构,拓展性能压测与飞书通知,附GitHub源码和踩坑记录

本文记录了一个完整的电商接口自动化测试项目从设计到落地的全过程,涵盖三层架构、Fixture 依赖注入、数据驱动和 Allure 报告等核心实践,并拓展了性能压测与飞书通知能力。

一、项目背景

最近在学习接口自动化测试,决定拿开源电商系统 OpenCart 4.1.0.3 练手。目标很明确:

  1. 覆盖核心业务流:商品浏览 → 加购 → 购物车查看
  2. 工程化:不是写几个脚本跑通就行,而是要体现分层设计、数据驱动、报告输出
  3. 可维护:需求变了(比如商品 ID 改了),改一处即可

最终技术栈定为:Python + requests + pytest + Allure。


二、整体架构设计

没有架构的测试项目,脚本写到 20 个就崩了。我参考了 Page Object 的思想,把项目拆成三层:

┌─────────────────────────────────────────┐
│  tests/          测试用例层              │
│  只写断言,不碰 HTTP 细节               │
├─────────────────────────────────────────┤
│  api/            业务 API 层             │
│  封装商品/购物车接口,支持依赖注入       │
├─────────────────────────────────────────┤
│  api/client.py   HTTP 客户端层           │
│  Session 管理、Token 自动携带、重试、异常抛出│
├─────────────────────────────────────────┤
│  data/           测试数据中心            │
│  商品、分类、账号,一处修改全局生效      │
└─────────────────────────────────────────┘

为什么这么分?

  • 如果后端接口路径变了(比如加购接口的 URL 调整),只改 cart_api.py 一处,所有用例无感知
  • 如果测试环境从 localhost 切到 192.168.x.x,只改 BASE_URL 一处
  • 如果商品 ID 变了,只改 test_data.py 一处

三、核心技术实现

3.1 HTTP 客户端:Session + 重试 + Token 自动拼接 + 异常校验

OpenCart 4.1.0.3 鉴权不再单纯依靠 Cookie,登录成功会返回 customer_token,后续请求需要在 URL 参数携带该 token。

⚠️ 注意:X-Requested-With 请求头不要全局配置。全局添加会让 GET 页面请求直接返回 JSON,拿不到 HTML 页面内容。该请求头应仅在 AJAX 接口单独携带。

class APIClient:
    BASE_URL = os.getenv("OPENCART_BASE_URL", "http://127.0.0.1/opencart")
    DEFAULT_TIMEOUT = 10

    def __init__(self):
        self.session = requests.Session()
        self.customer_token = None  # 存储 4.x 登录凭证
        retries = Retry(total=3, backoff_factor=0.5, status_forcelist=(500, 502, 503, 504))
        self.session.mount("http://", HTTPAdapter(max_retries=retries))
        self.session.mount("https://", HTTPAdapter(max_retries=retries))

    def _attach_token(self, path: str) -> str:
        """自动给请求 URL 拼接 customer_token 登录凭证"""
        if self.customer_token:
            sep = "&" if "?" in path else "?"
            return f"{path}{sep}customer_token={self.customer_token}"
        return path

    def _check_status(self, resp):
        """重试耗尽后,5xx 服务端错误直接抛异常"""
        if resp.status_code >= 500:
            raise requests.HTTPError(f"服务端错误 {resp.status_code}: {resp.url}")
        return resp

get() 和 post() 方法在调用前会先执行 _attach_token() 拼接凭证,请求返回后再执行 _check_status() 校验状态码。

亮点:

  • Session 管理会话,_attach_token 自动拼接登录凭证,业务层只管调接口,不用手动传 token
  • Retry 机制提升稳定性,避免偶发网络抖动导致用例失败;重试耗尽后 5xx 直接抛异常
  • BASE_URL 通过环境变量注入,支持多环境切换

3.2 业务 API 层:依赖注入

以购物车模块为例,CartAPI 不自己创建客户端,而是接受外部传入:

class CartAPI:
    def __init__(self, client: APIClient = None):
        self.client = client or APIClient()
        
    def add_to_cart(self, product_id: int, quantity: int = 1):
        # AJAX接口,此处单独添加异步请求头
        return self.client.post(
            "/index.php?route=checkout/cart/add",
            data={"product_id": product_id, "quantity": quantity},
            headers={"X-Requested-With": "XMLHttpRequest"}
        )

为什么这么设计?

测试用例里可以灵活选择:

  • 传已登录的 client → 测需要鉴权的接口(加购、查看购物车)
  • 不传(默认新建)→ 测游客场景

3.3 Fixture 管理登录态

OpenCart 4.1.0.3 登录不是一步 POST,分为四步:

1、GET 访问登录页面,提取页面内一次性 login_token;

2、AJAX 接口提交账号密码,login_token 放在 URL 查询参数,携带异步请求头;

3、解析返回的 redirect 链接,提取 customer_token 存入 client 实例;

4、访问个人中心页面做断言校验,确认登录态真正生效。

# conftest.py
import re

@pytest.fixture
def api_client():
    client = APIClient()
    # 1. GET登录页面获取一次性login_token
    login_page = client.get("/index.php?route=account/login&language=en-gb")
    login_token = re.search(r'login_token=([a-f0-9]+)', login_page.text).group(1)

    # 2. AJAX接口提交账号密码,login_token放在url参数,body只传账号密码
    resp = client.post(
        f"/index.php?route=account/login.login&language=en-gb&login_token={login_token}",
        data=TEST_USER,
        headers={"X-Requested-With": "XMLHttpRequest"}
    )
    redirect_url = resp.json()["redirect"]

    # 3. 解析拿到customer_token,存入client用于后续请求自动携带
    client.customer_token = re.search(r'customer_token=([a-f0-9]+)', redirect_url).group(1)

    # 4. 登录结果校验,提前拦截登录失败,区分环境问题和业务bug
    verify = client.get("/index.php?route=account/account&language=en-gb")
    assert "account/login" not in verify.url, f"登录校验失败,重定向地址:{verify.url}"

    return client

测试用例只需声明参数即可使用:

def test_add_to_cart_success(self, api_client):
    api = CartAPI(api_client)
    resp = api.add_to_cart(PRODUCTS["macbook"]["product_id"], 1)
    assert resp.status_code == 200
    assert "Success" in resp.text

代码量减少 40%,而且登录逻辑集中管理,更换测试账号仅改测试数据文件。

3.4 数据驱动:test_data.py

所有硬编码数据抽到独立模块:

# data/test_data.py
TEST_USER = {
    "email": "testuser01@demo.local",
    "password": "Test@123456"
}

PRODUCTS = {
    "macbook": {"product_id": 43, "name": "MacBook"},
    "iphone": {"product_id": 40, "name": "iPhone"},
}

CATEGORIES = {
    "desktops": 20,
    "software": 17,
}

用例里通过常量引用:PRODUCTS["macbook"]["product_id"],而不是裸写 43。

3.5 Allure 报告分级

用 @allure.epic / feature / story 做三级注解,生成结构化的报告:

@allure.epic("OpenCart 接口测试")
@allure.feature("购物车接口")
class TestCartAPI:

    @allure.story("加购")
    @allure.title("TC-API-CART-001: 正常加购")
    @allure.severity(allure.severity_level.CRITICAL)
    def test_add_to_cart_success(self, api_client):
        ...

报告效果:

  • 项目级 → 模块级 → 功能级,层层展开
  • 用例带编号、带优先级,方便追溯

四、典型用例解析

以购物车加购为例,完整调用链路:

pytest 发现 test_add_to_cart_success 依赖 api_client fixture
        ↓
conftest.py 创建 APIClient → 执行四步登录 → 提取 customer_token 存入实例
        ↓
CartAPI(api_client) 注入已登录客户端
        ↓
add_to_cart(43, 1) → POST checkout/cart/add,携带 AJAX 请求头
        ↓
APIClient 的 _attach_token 自动在 URL 拼接 customer_token 凭证
        ↓
断言:status_code == 200 且响应包含 "Success"

整个过程测试层只关注业务断言,HTTP 细节、登录逻辑、Token 管理全部下沉到底层。


五、踩坑记录

1. 环境部署篇

本地部署 OpenCart 时,MySQL 反复启动失败,报错 ibdata1 大小不匹配。

排查过程:

  1. 发现 my.ini 里 datadir 指向了错误的 c:/xampp/mysql/data
  2. 清理损坏的 InnoDB 文件(ibdata1、ib_logfile*)
  3. 重置 root 密码(MariaDB 10.4 的权限表结构变了,不能用传统 UPDATE user SET password)
  4. 重新安装 OpenCart,数据库恢复正常

教训:本地环境出问题不要慌,看日志、看配置、看端口,逐步缩小范围。

2. 接口路由篇

调试 OpenCart 登录接口时,请求始终返回 404。

排查过程:

  1. 检查接口路径是否正确 → 路径没问题
  2. 检查服务器是否正常 → 首页能打开,服务正常
  3. 抓包对比浏览器请求 → 发现浏览器发的是 account/login.login,我写的是 account/login

根因:OpenCart 4.1.0.3 存在两套路由规则

  • 页面路由用斜杠:route=account/login
  • AJAX 接口用点分:route=account/login.login

两者不可混用。登录、加购这类通过 AJAX 提交的接口,必须用点分格式;直接 GET 的页面,用斜杠格式。

修复方式:

# ❌ 错误写法(AJAX 接口写成页面路由格式,404)
"/index.php?route=account/login"

# ✅ 正确写法(AJAX 接口用点分格式)
"/index.php?route=account/login.login"

教训: 接口自动化测试不能只看文档,要结合目标系统的实际路由解析规则。遇到 404 优先抓包对比浏览器真实请求,而不是盲目改参数。OpenCart 4 的两套路由机制,光看文档很容易踩坑。

3. Locust 报告编码篇

飞书通知脚本一直读不到 Locust 的 CSV 数据,排查半天发现是文件编码问题。Windows 下 Locust 生成的 CSV 文件是 gbk 编码,而脚本默认用 utf-8 读取,导致解析失败。

修复方式:

在读取 CSV 时自动检测编码,依次尝试多种编码格式:

encodings = ['utf-8', 'gbk', 'gb2312', 'latin-1']
for enc in encodings:
    try:
        with open(csv_path, 'r', encoding=enc) as f:
            lines = f.readlines()
            # 解析数据...
            break
    except UnicodeDecodeError:
        continue

教训: 跨平台开发时要注意文件编码差异,Windows 下生成的文件不一定是 UTF-8,读取时要做兼容处理。

4. JMeter 报告覆盖篇

批量运行压测时,JMeter 报错 Cannot write to folder as folder is not empty,导致 100 并发报告生成失败。

根因:JMeter 默认不允许覆盖已存在的报告目录。

修复方式:

在 JMeter 命令末尾加上 -f 参数,强制覆盖已有目录:

jmeter -n -t jmeter/opencart_category_100vu.jmx -l jmeter/result_100vu.jtl -e -o jmeter/report_100vu -f

教训: 批量运行压测时记得加 -f,或者每次运行前先删除旧的报告目录。


六、技术拓展:性能压测 + 飞书通知

接口自动化验证功能正确性,性能压测验证系统稳定性,通知推送解决“跑完测试还要手动翻报告”的问题。

6.1 双工具压测(JMeter + Locust)

  • JMeter:50/100 并发梯度加压,生成 HTML 报告
  • Locust:15/25 并发代码化压测,生成 CSV 数据

测试数据示例(本地 XAMPP 环境):

工具并发数总请求数失败率平均响应时间P95
JMeter50103,6200.00%25.03 ms110 ms
JMeter100109,0680.00%47.69 ms264 ms
Locust158360188.24 ms—
Locust251,3550198.46 ms—

详细压测过程见系列第三篇。

6.2 飞书自动通知

测试完成后自动解析 JMeter/Locust 报告,推送结构化消息到飞书群,包含总请求数、失败率、平均响应时间、P95、吞吐量等关键指标。

Snipaste_2026-09-06_21-52-42.png

配置方式:在飞书群添加自定义机器人,获取 Webhook 地址,填入 send_feishu_report.py 即可。

七、总结

这个项目虽然不大,但覆盖了接口自动化测试的核心能力:

能力体现
分层架构client → api → tests 三层分离
设计模式API 层的依赖注入、数据驱动
框架特性pytest fixture、Allure 报告分级
工程化环境变量解耦、Token 自动处理、一键运行脚本

系列文章


完整源码:opencart-api-test

下一步计划:完善性能压测的 CI 集成,实现定时触发和报告自动归档。