ECShop电商系统接口自动化测试实战:从环境搭建到CI/CD集成
1. 项目概述与核心价值
最近在带新人做软件测试的实战项目,发现很多同学对“如何从一个零散的项目标题,落地为一套完整的、可交付的测试资产”这个过程感到迷茫。正好,手头有一个非常经典的实战案例——“ECShop电子商务系统软件测试作业”。这个标题看似简单,只列出了几项交付物:搭建文档、接口测试用例、接口文档和测试脚本。但如果你真的以为这只是把四个文件拼凑起来,那就大错特错了。这背后,其实是一套完整的、从环境部署到自动化验证的软件测试工程化实践。对于想入行软件测试,或者希望从功能测试转向测试开发的同学来说,把这个项目吃透,价值远超做几十道八股文面试题。
ECShop作为一个开源的老牌电商系统,其架构(典型的LAMP/ LNMP)和功能模块(商品、订单、会员、支付)在电商领域极具代表性。以它作为测试对象,你几乎能接触到Web系统测试的所有核心场景:环境搭建与配置、前后端功能测试、数据库验证,以及重头戏——接口测试。而“接口测试”正是当前企业招聘时非常看重的技能点,从Postman、Apifox的手工测试,到使用Python+Requests或JMeter编写自动化脚本,再到思考如何与CI/CD流水线集成,这一条技术链正是本次项目的精髓所在。所以,这个项目不只是完成作业,更是你构建个人技术栈、打造求职项目经验的绝佳机会。接下来,我将以从业者的视角,为你彻底拆解如何高质量地完成这个项目,其中会包含大量在官方文档里不会写的“踩坑”经验和工程化思考。
2. 环境搭建:从零构建可测试的ECShop系统
2.1 技术栈选型与准备工作
在开始写任何测试用例之前,一个稳定、干净的测试环境是基石。ECShop虽然老旧,但其环境依赖非常明确。为了避免后续出现各种诡异问题,我强烈建议使用虚拟化或容器化技术来隔离环境。
我的选择是Docker Compose 。原因有三:第一,它能把PHP、MySQL、Nginx等组件及其版本、配置全部固化在 docker-compose.yml 文件中,实现环境即代码,在任何机器上都能一键复现。第二,完全与宿主机环境隔离,测试结束后销毁容器,不会留下任何垃圾。第三,非常适合模拟不同的系统配置(如切换PHP版本)进行兼容性测试。
你需要准备以下工具:
- Docker Desktop :用于运行容器。确保在安装后开启WSL2或Hyper-V支持。
- Git :用于克隆ECShop源码。
- 一款趁手的IDE :VSCode或JetBrains系列均可,用于编写脚本和文档。
- 浏览器及开发者工具 :Chrome或Edge,用于前端操作和抓取接口。
注意 :切勿在个人常用的开发机或服务器上直接安装LAMP环境来部署ECShop,版本冲突、依赖污染会让你在后续步骤中痛苦不堪。Docker是我们的“安全沙盒”。
2.2 基于Docker的ECShop部署实操
这里我分享一个经过验证的 docker-compose.yml 配置,它包含了ECShop v4.1版本所需的所有服务。
version: '3.8'
services:
mysql:
image: mysql:5.7
container_name: ecshop-mysql
environment:
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: ecshop
MYSQL_USER: ecshop_user
MYSQL_PASSWORD: ecshop_pass
volumes:
- ./mysql_data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "3307:3306"
networks:
- ecshop-network
php:
build: ./php
container_name: ecshop-php
volumes:
- ./ecshop:/var/www/html
- ./php/php.ini:/usr/local/etc/php/php.ini
depends_on:
- mysql
networks:
- ecshop-network
nginx:
image: nginx:1.20-alpine
container_name: ecshop-nginx
ports:
- "8080:80"
volumes:
- ./ecshop:/var/www/html
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
networks:
- ecshop-network
networks:
ecshop-network:
driver: bridge
关键配置解析与避坑指南:
-
MySQL服务 :
image: mysql:5.7:ECShop对MySQL 8.0+的兼容性可能有问题,5.7是最稳妥的选择。- 端口映射为
3307:3306:避免与宿主机可能已存在的MySQL服务(默认3306)冲突。后续测试脚本连接数据库时,主机地址应为localhost:3307。 volumes中的init.sql:这是一个初始化脚本,用于创建数据库和用户。内容如下:CREATE DATABASE IF NOT EXISTS `ecshop` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; GRANT ALL PRIVILEGES ON `ecshop`.* TO 'ecshop_user'@'%' IDENTIFIED BY 'ecshop_pass'; FLUSH PRIVILEGES;
-
PHP服务 :这里我们选择自定义构建。在项目根目录创建
php/Dockerfile:FROM php:7.4-fpm # 安装ECShop必需的扩展 RUN docker-php-ext-install mysqli pdo_mysql gd RUN apt-get update && apt-get install -y libpng-dev libjpeg-dev libfreetype6-dev RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j$(nproc) gd- 为什么选PHP 7.4?这是兼顾ECShop兼容性和现代PHP特性的一个平衡点。务必安装
gd扩展,否则后台验证码无法显示。 - 将自定义的
php.ini(如调整上传文件大小、内存限制)挂载到容器中。
- 为什么选PHP 7.4?这是兼顾ECShop兼容性和现代PHP特性的一个平衡点。务必安装
-
Nginx服务 :配置
nginx/default.conf,这是关键的一步,配置错误会导致访问404或PHP文件被下载。server { listen 80; server_name localhost; root /var/www/html; index index.php index.html index.htm; location / { try_files $uri $uri/ /index.php?$query_string; } location ~ \.php$ { fastcgi_pass php:9000; # 注意这里指向的是php服务名和端口 fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } }fastcgi_pass php:9000;:这里的php是Docker Compose网络中的服务名,Docker会将其解析为PHP容器的IP。这是容器间通信的标准方式,比用IP地址更稳定。
部署步骤:
- 在项目根目录执行
docker-compose up -d。 - 访问
http://localhost:8080/install,按照ECShop的安装向导完成安装。数据库主机填写mysql(同样是服务名),端口3306(容器内端口)。 - 安装成功后,访问
http://localhost:8080和http://localhost:8080/admin确认前后台可正常访问。
实操心得 :安装完成后,务必第一时间备份
/ecshop/data/config.php和数据库。这是你的测试基准环境。任何破坏性测试(如压力测试、异常数据测试)前,都应先恢复到这个快照。
3. 接口文档分析与测试用例设计
3.1 逆向工程:如何获取与分析接口
ECShop作为传统PHP项目,通常没有提供标准的Swagger或OpenAPI接口文档。我们的“接口文档”需要自己通过分析来生成。这是测试工程师非常重要的能力——在没有文档的情况下摸清系统脉络。
方法一:浏览器开发者工具抓包(最直接)
- 打开Chrome开发者工具(F12),切换到 Network(网络) 标签页。
- 勾选 Preserve log(保留日志) ,防止页面跳转后请求记录被清空。
- 在ECShop前台进行关键操作:登录、搜索商品、加入购物车、下单、支付(模拟)。
- 观察捕获到的请求,重点关注
XHR或Fetch类型的请求,这些通常是API接口。例如:user.php?act=login(POST) - 用户登录flow.php?step=add_to_cart(POST) - 添加购物车order.php?act=submit(POST) - 提交订单
方法二:分析源代码(最彻底) 直接阅读 /api/ 、 /includes/ 目录下的PHP文件,特别是 cls_json.php 和 api 开头的文件。ECShop的API通常以 api.php?act=xxx 的形式调用,参数通过 POST 或 GET 传递,返回JSON格式数据。例如,查看 api/client/api.php ,你能找到所有客户端API的定义。
方法三:使用代理工具(最专业) 使用Fiddler或Charles这类抓包工具,可以捕获包括移动端在内的所有HTTP/HTTPS流量,并进行重放和篡改测试,对于分析接口行为非常高效。
生成你的接口文档 : 将分析结果整理成一份Markdown或Excel表格。一份合格的接口文档应包含:
- 接口名称 :如“用户登录接口”
- 请求URL :
http://localhost:8080/user.php - 请求方法 :
POST - 请求参数 :
参数名 类型 是否必填 描述 示例 actString 是 操作类型 loginusernameString 是 用户名 testuserpasswordString 是 密码 md5(‘123456’)rememberInt 否 记住登录 1 - 响应示例(成功) :
{ "code": 1, "message": "登录成功", "data": { "user_id": 5, "user_name": "testuser" } } - 响应示例(失败) :
{ "code": 0, "message": "用户名或密码错误" } - 备注 :如加密方式(ECShop密码常用MD5)、Session依赖等。
3.2 设计高覆盖率的接口测试用例
有了接口文档,就可以设计测试用例了。切忌只测“正常流”。一个好的测试用例集应该像一张网,覆盖各种场景。我们可以从以下几个维度设计:
1. 功能维度(针对单个接口)
- 正向用例 :使用正确的参数,验证接口功能是否正常。这是最基本的。
- 边界值分析 :针对数字、长度等参数。例如,用户名字段长度边界是3-20位,那么测试用例就应包括:2位(失败)、3位(成功)、20位(成功)、21位(失败)。
- 异常参数 :
- 必填参数缺失 :不传
username或password。 - 参数类型错误 :
remember传字符串“true”而非数字1。 - 参数格式错误 :邮箱字段传入非法字符串。
- SQL注入/XSS尝试 :在参数中传入
‘ or ‘1’=‘1或<script>alert(1)</script>,验证系统是否有防护。
- 必填参数缺失 :不传
- 业务规则验证 :例如,登录接口的“记住我”功能是否真的延长了Session时间;添加购物车时,库存不足是否被正确处理。
2. 流程维度(串联多个接口) 电商核心流程: 注册/登录 -> 浏览商品 -> 加入购物车 -> 修改购物车 -> 生成订单 -> 支付 -> 查看订单状态 。我们需要设计端到端的流程用例,确保接口之间的数据传递和状态变更正确。例如,下单接口( order.php?act=submit )依赖购物车接口( flow.php?step=cart )产生的临时购物车ID。
3. 安全与性能维度
- 安全 :重复提交订单(幂等性)、越权访问(尝试修改他人订单)、敏感信息泄露(响应中是否包含明文密码)。
- 性能 :使用JMeter或
locust对关键接口(如商品列表查询、下单接口)进行压力测试,评估响应时间和吞吐量。
用例管理建议 : 使用Excel或专业的测试管理工具(如Tapd、禅道)来管理用例。表格应包含:用例ID、模块、接口名称、用例标题、前置条件、请求方法/URL、请求头、请求参数、预期响应状态码、预期响应体、实际结果、测试人员、测试日期等字段。清晰的用例是后续编写自动化脚本的蓝图。
4. 接口测试脚本开发实战
4.1 工具选型:Postman vs. 代码脚本
对于这个项目,我建议采用 “Postman(或Apifox)用于调试与文档协作 + Python + Requests + Pytest用于自动化脚本” 的组合拳。
- Postman/Apifox :图形化界面友好,非常适合在测试设计阶段快速调试接口、构造各种参数、查看响应。它们的环境变量、集合运行、数据驱动测试功能也很强大,可以作为轻量级的自动化工具。但对于需要复杂逻辑(如数据库校验、加解密、集成到CI/CD)的场景,代码脚本更灵活。
- Python + Requests + Pytest :这是目前业界主流的接口自动化测试框架组合。Requests库简洁强大,Pytest提供了丰富的夹具(fixture)、参数化、断言和插件生态,测试报告美观。代码易于版本管理(Git),也方便与Jenkins、GitLab CI等工具集成。
为什么选择Python而不是JMeter? JMeter在性能测试领域是王者,但对于复杂的业务逻辑断言、数据库查询、动态参数处理,其BeanShell或JSR223脚本的编写和调试体验远不如Python友好。我们的项目重点是功能接口测试,Python脚本的可读性和可维护性更高。
4.2 基于Pytest的自动化测试框架搭建
让我们搭建一个结构清晰、易于维护的测试框架。
项目目录结构 :
ecshop_api_test/
├── conftest.py # Pytest全局配置、夹具定义
├── requirements.txt # 项目依赖
├── config/ # 配置文件
│ └── config.py # 环境配置(测试URL、数据库连接等)
├── common/ # 公共模块
│ ├── __init__.py
│ ├── request_client.py # 封装的Requests客户端
│ └── db_client.py # 数据库操作封装
├── test_data/ # 测试数据文件
│ ├── user_data.yaml # 用户相关测试数据
│ └── order_data.yaml
├── test_cases/ # 测试用例目录
│ ├── __init__.py
│ ├── test_user.py # 用户模块测试
│ └── test_order.py # 订单模块测试
└── reports/ # 测试报告目录(自动生成)
1. 核心模块封装 ( common/request_client.py ) 不要在每个测试用例里都写 requests.post() ,封装一个客户端,统一处理日志、异常和基础断言。
import requests
import logging
from typing import Any, Dict, Optional
class ApiClient:
def __init__(self, base_url: str):
self.base_url = base_url.rstrip('/')
self.session = requests.Session()
self.logger = logging.getLogger(__name__)
def request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
url = f"{self.base_url}/{endpoint.lstrip('/')}"
self.logger.info(f"Request: {method} {url}")
self.logger.debug(f"Request kwargs: {kwargs}")
try:
resp = self.session.request(method, url, **kwargs)
self.logger.info(f"Response Status: {resp.status_code}")
self.logger.debug(f"Response Body: {resp.text}")
except requests.exceptions.RequestException as e:
self.logger.error(f"Request failed: {e}")
raise
return resp
def post_form(self, endpoint: str, data: Dict[str, Any]) -> requests.Response:
"""针对ECShop常见表单提交的封装"""
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
return self.request('POST', endpoint, data=data, headers=headers)
def assert_status_code(self, response: requests.Response, expected_code: int):
assert response.status_code == expected_code, \
f"Expected status {expected_code}, but got {response.status_code}. Response: {response.text}"
2. 测试用例编写示例 ( test_cases/test_user.py ) 使用Pytest的夹具来管理测试前后的动作,如登录获取Token。
import pytest
from common.request_client import ApiClient
from config.config import TEST_CONFIG
class TestUserApi:
@pytest.fixture(scope="class")
def api_client(self):
"""返回一个配置好基础URL的API客户端"""
return ApiClient(TEST_CONFIG['base_url'])
@pytest.fixture
def login_session(self, api_client):
"""登录并返回一个已登录的session客户端,用于需要登录态的测试"""
client = ApiClient(TEST_CONFIG['base_url'])
login_data = {
'act': 'login',
'username': TEST_CONFIG['test_user']['username'],
'password': TEST_CONFIG['test_user']['password_md5'] # ECShop密码常为MD5
}
resp = client.post_form('user.php', data=login_data)
# 假设登录成功会在响应头或Cookie中返回session信息,这里需要根据实际情况处理
# 例如,ECShop可能使用Cookie: ECS_ID
assert resp.status_code == 200
assert '登录成功' in resp.text
return client # 返回已携带登录态Cookie的client
def test_login_success(self, api_client):
"""测试用户登录成功"""
data = {
'act': 'login',
'username': 'correct_user',
'password': 'e10adc3949ba59abbe56e057f20f883e' # '123456'的MD5
}
resp = api_client.post_form('user.php', data=data)
api_client.assert_status_code(resp, 200)
resp_json = resp.json()
assert resp_json['code'] == 1
assert 'user_id' in resp_json.get('data', {})
@pytest.mark.parametrize("username, password, expected_msg", [
("wrong_user", "e10adc3949ba59abbe56e057f20f883e", "用户名或密码错误"),
("correct_user", "wrong_md5", "用户名或密码错误"),
("", "e10adc3949ba59abbe56e057f20f883e", "用户名不能为空"), # 假设的返回
])
def test_login_failure(self, api_client, username, password, expected_msg):
"""参数化测试登录失败场景"""
data = {'act': 'login', 'username': username, 'password': password}
resp = api_client.post_form('user.php', data=data)
api_client.assert_status_code(resp, 200)
assert expected_msg in resp.text
def test_add_to_cart_requires_login(self, api_client, login_session):
"""测试添加购物车需要登录态:未登录应失败,登录后应成功"""
# 1. 使用未登录的客户端尝试添加购物车
data = {'act': 'add_to_cart', 'goods_id': 1, 'number': 1}
resp = api_client.post_form('flow.php', data=data)
# 根据ECShop逻辑,可能返回错误码或跳转到登录页
assert resp.status_code != 200 or '请先登录' in resp.text
# 2. 使用已登录的客户端尝试添加购物车
resp_logged = login_session.post_form('flow.php', data=data)
login_session.assert_status_code(resp_logged, 200)
assert '添加成功' in resp_logged.text
3. 数据库校验的集成 真正的自动化测试,不仅要看接口返回,还要验证数据是否正确地落库了。
# common/db_client.py
import pymysql
from config.config import DB_CONFIG
class DBClient:
def __init__(self):
self.connection = pymysql.connect(**DB_CONFIG)
self.cursor = self.connection.cursor(pymysql.cursors.DictCursor)
def query_one(self, sql: str, args=None):
self.cursor.execute(sql, args)
return self.cursor.fetchone()
def close(self):
self.cursor.close()
self.connection.close()
# 在测试用例中使用
def test_order_submit_updates_database(login_session):
"""测试提交订单后,数据库中的订单表和订单商品表是否正确更新"""
db = DBClient()
# 1. 获取提交订单前的数据快照(例如,用户的订单数量)
sql_before = "SELECT COUNT(*) as order_count FROM ecs_order_info WHERE user_id = %s"
before_result = db.query_one(sql_before, (TEST_USER_ID,))
order_count_before = before_result['order_count']
# 2. 调用提交订单接口
order_data = {...} # 构造订单数据
resp = login_session.post_form('order.php?act=submit', data=order_data)
assert resp.status_code == 200
# 3. 验证数据库
after_result = db.query_one(sql_before, (TEST_USER_ID,))
order_count_after = after_result['order_count']
assert order_count_after == order_count_before + 1
# 还可以进一步查询最新订单的详细信息进行断言
sql_new_order = "SELECT * FROM ecs_order_info WHERE user_id = %s ORDER BY order_id DESC LIMIT 1"
new_order = db.query_one(sql_new_order, (TEST_USER_ID,))
assert new_order['order_sn'] is not None
assert new_order['order_amount'] == EXPECTED_AMOUNT
db.close()
4.3 测试数据管理与参数化
硬编码的测试数据是自动化脚本的“毒药”。我们需要将数据与代码分离。
使用YAML文件管理测试数据 ( test_data/user_data.yaml ) :
login:
success:
- username: "test_user_1"
password_md5: "e10adc3949ba59abbe56e057f20f883e" # 123456
expected_user_id: 5
failure:
- username: ""
password_md5: "e10adc3949ba59abbe56e057f20f883e"
expected_msg: "用户名不能为空"
- username: "not_exist"
password_md5: "e10adc3949ba59abbe56e057f20f883e"
expected_msg: "用户名或密码错误"
在测试用例中读取YAML数据 :
import yaml
import pytest
def load_test_data(file_name, key):
with open(f'test_data/{file_name}', 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
return data[key]
class TestUserLogin:
@pytest.mark.parametrize("case", load_test_data('user_data.yaml', 'login.success'))
def test_login_success_parametrized(self, api_client, case):
data = {'act': 'login', 'username': case['username'], 'password': case['password_md5']}
resp = api_client.post_form('user.php', data=data)
api_client.assert_status_code(resp, 200)
resp_json = resp.json()
assert resp_json['data']['user_id'] == case['expected_user_id']
5. 测试执行、报告与持续集成
5.1 测试执行与报告生成
编写好脚本后,在项目根目录下执行测试:
# 运行所有测试
pytest
# 运行特定模块
pytest test_cases/test_user.py
# 运行带有特定标记的测试
pytest -m "login"
# 生成详细的HTML报告
pytest --html=reports/report.html --self-contained-html
使用 pytest-html 插件生成的报告,会清晰展示用例通过率、失败详情、日志输出,非常适合归档和分享。
5.2 集成到CI/CD流水线(进阶)
要让测试价值最大化,就应该让它自动运行。这里给出一个最简单的GitLab CI .gitlab-ci.yml 示例:
stages:
- test
api-test:
stage: test
image: python:3.9-slim
before_script:
- pip install -r requirements.txt
script:
- echo "开始执行接口自动化测试..."
- pytest --html=report.html
artifacts:
when: always
paths:
- report.html
expire_in: 1 week
only:
- merge_requests # 仅在合并请求时触发
- main # 推送到主分支时也触发
这样,每次有代码合并请求时,都会自动运行接口测试,并将报告附加到MR中,方便代码审查者直观地看到改动是否影响了现有功能。
5.3 常见问题排查与技巧实录
在实际操作中,你一定会遇到各种问题。这里记录几个高频“坑点”:
-
接口返回乱码或中文乱码 :
- 原因 :ECShop页面编码通常是GBK或UTF-8,而Requests默认使用ISO-8859-1解码。
- 解决 :在封装请求客户端时,强制指定响应编码:
resp.encoding = 'utf-8'或resp.encoding = resp.apparent_encoding。或者在Nginx/PHP配置中统一设置为UTF-8。
-
Session/Cookie失效问题 :
- 现象 :第一个登录接口成功,第二个需要登录态的接口失败。
- 排查 :检查
ApiClient是否使用了requests.Session()来保持会话。确保登录后返回的Cookie被正确保存在Session中,并在后续请求中自动携带。 - 技巧 :在
request_client.py的request方法中加入日志,打印每次请求的Cookie信息。
-
验证码处理 :
- 难题 :登录、注册等接口可能有图形验证码。
- 策略 :
- 测试环境关闭验证码 :这是最常用的方法。修改ECShop源码或数据库配置,在测试环境禁用验证码功能。
- 万能验证码 :在代码中设置一个后端认可的“万能验证码”。
- OCR识别(不推荐) :复杂且不稳定,仅作为最后手段。
-
数据库连接失败 :
- 现象 :
pymysql无法连接到Docker容器中的MySQL。 - 排查 :
- 确认
DB_CONFIG中的主机是localhost还是容器服务名?从宿主机连接需用localhost:3307,从另一个容器(如测试脚本容器)连接需用服务名mysql:3306。 - 检查MySQL用户权限:是否允许从测试脚本所在主机/IP连接。
- 防火墙或安全组设置。
- 确认
- 现象 :
-
依赖安装失败 :
- 特别是
mysqlclient或pymysql在Windows上可能依赖C++编译工具。 - 解决 :可以使用预编译的轮子,或者安装
pip install mysql-connector-python,这是一个纯Python实现的MySQL驱动。
- 特别是
完成这个项目后,你得到的远不止是四个文档。你拥有了一套完整的、可复用的电商系统接口自动化测试解决方案,包括环境搭建、接口分析、用例设计、脚本开发、框架搭建和持续集成实践。这套方法论和经验,完全可以平移到其他任何Web项目的测试中。记住,测试工程师的核心价值不在于点了多少下鼠标,而在于能否通过技术手段,高效、可靠、持续地保障产品质量。这个项目,就是你迈向这个目标的一块坚实垫脚石。
更多推荐




所有评论(0)