电商购物平台测试
一、报告概述
1、项目背景
ShopXO 商城系统是一款基于 Web 的开源企业级 B2C 电商平台,主要用于:商品展示与销售、在线购物与下单、订单管理与支付、用户中心与个人数据管理。该系统面向中小型企业及个体商户,提供从商品上架、用户浏览、购物车管理、下单支付到订单履约的全流程电商解决方案。
系统包含以下核心模块:
- 用户管理:注册、登录、权限控制、个人中心
- 商品管理:商品展示、分类导航、搜索筛选、详情查看
- 购物车:商品加购、数量调整、勾选结算
- 订单管理:结算下单、订单查询、状态跟踪、批量操作
- 收货地址:地址新增、编辑、删除、默认设置
- 支付管理:货到付款、支付方式管理
- 收藏管理:商品收藏、收藏列表
- 消息管理:系统消息、已读未读筛选
- 用户中心:订单概览、快捷入口、个人资料
2、测试目的
1)功能验证
① 确保所有核心功能符合要求
② 验证用户注册/登录流程的正确性,含图形验证码识别
③ 确认商品浏览、搜索、分类筛选的完整流程
④ 检查购物车管理和结算下单的功能完整性
⑤ 验证订单管理和状态查询的可用性
⑥ 验证收货地址、收藏、消息等辅助功能的正确性
⑦ 验证货到付款支付方式的真实下单流程
2)用户体验优化
① 评估页面布局的合理性和易用性
② 测试导航逻辑与菜单跳转是否符合用户习惯
③ 验证表单校验与错误提示信息的清晰度
④ 检查弹窗交互(登录弹窗、确认弹窗)的响应表现
3)系统稳定性保障
① 评估系统在页面加载、图片资源加载场景下的稳定性
② 验证异常情况(元素定位失效、页面跳转异常)下的系统行为
③ 检查购物车数据一致性与测试数据还原机制
④ 验证登录态会话管理的可靠性
4)接口测试
① 确保核心业务接口的请求参数与响应结构符合定义
② 验证登录、购物车、订单等接口的鉴权与状态管理
③ 检查接口在异常输入下的容错处理
④ 验证 UI 层与接口层的数据一致性
5)回归测试保障
① 建立可重复执行的自动化用例集,为后续版本迭代提供回归基线
② 覆盖核心购物链路,确保关键业务流程不回归
3、测试环境
操作系统:Windows 11 / Windows 10
浏览器:Chrome、Edge、Firefox
测试工具:Selenium、Pytest、Requests、Allure
二、测试用例

图片链接:测试用例
三、手动测试
1、注册页面功能
测试场景一:账号注册,输入用户名,登录密码,验证码后同意用户协议,进行注册
预期结果:注册成功,跳转到电商平台首页



测试场景二:账号注册,不输入用户名,但输入密码,验证码,同意用户协议
预期结果:提示输入用户名不符合要求

测试场景三:输入已存在用户名,进行注册
预期结果:提示用户名已存在

测试场景四:未输入登录密码
预期结果:提示输入登录密码不符合要求

2、登录页面功能
测试场景一:输入正确的登录账号和登录密码,验证码,点击登录
预期结果:登录成功,直接跳转到电商平台首页


测试场景二:输入错误的登录账号和错误的登录密码,正确的验证码,点击登录
预期结果:提示账号不存在

测试场景三:输入正确的登录账号,错误的登录密码,正确的验证码,进行登录
预期结果:提示密码错误

测试场景四:输入正确的的登录账号,不输入登录密码,正确的验证码,进行登录
预期结果:提示密码格式错误

测试场景五:不输入登录账号,输入正确的登录密码,正确的验证码,点击登录
预期结果:提示请输入登录账号

测试场景六:输入正确的登录账号和正确的登录密码,不输入验证码,点击登录
预期结果:提示请输入有效的图形验证码

3、商城首页页面功能
测试场景一:对页面上的轮播图进行手动切换
预期结果:页面上的轮播图能够进行转换,由当前轮播图转换为下一张轮播图


测试场景二:点击“退出”按钮
预期结果:成功退出,提示,跳转回首页,未登录状态


4、商品搜索页面功能
测试场景一:在搜索框中进行搜索,点击“搜索”按钮
预期结果:在当前页面刷新页面进行更新


测试场景二:点击搜索框下的预置搜索词
预期结果:跳转至新的搜索页面


测试场景三:点击“品牌”中的选项进行筛选
预期结果:在当前页面刷新页面进行更新


测试场景四:点击“价格”,进行商品排序
预期结果:点击第一次,商品按降价进行排序;再点击一次,商品按照升序进行排序


5、商品分类页面功能
测试场景一:点击一级分类图标
预期结果:展示出该一级分类选项下的所有商品


测试场景二:点击二级分类选项
预期结果:展示出该二级分类选项下的所有商品


测试场景三:点击三级分类选项
预期结果:展示出该三级分类选项下的所有商品

6、商品详情页面功能
测试场景一:选择规格/数量后,点击“加入购物车”按钮
预期结果:提示商品已成功加入购物车

测试场景二:点击“收藏”按钮
预期结果:提示:收藏成功

测试场景三:登录状态下,点击“立即购买”按钮
预期结果:跳转至订单确认页面


7、购物车页面功能
测试场景一:点击商品前的选中框,进行商品选中
预期结果:商品选中成功,选中框中出现对号,选中商品件数加上所选商品的件数,合计值加上被选中商品的价钱

测试场景二:商品收藏,点击商品前的选中框,选中商品,点击“收藏”按钮
预期结果:提示:收藏成功

测试场景三:点击“全选”前的选中框,进行商品全选操作
预期结果:所有选中框中都出现对号,“全选”变为“反选”,选中商品件数为购物车内所有商品的数量,合计值为购物车内所有商品的价钱

测试场景四:商品删除,点击商品前的选中框,选中商品,点击“删除”按钮
预期结果:提示:删除后无法恢复、确定继续吗?点击确认,删除成功



测试场景五:结算,选中商品,点击“结算”按钮
预期结果:跳转至订单确认页面


8、订单确认页面功能
测试场景一:确认收货地址,选择一个收货地址
预期结果:选中后会有选中框给地址框起来,代表已选中

测试场景二:选择支付方式,选择一种支付方式
预期结果:选中之后会有红框给支付方式框起来,代表已选中

测试场景三:提交订单,点击“提交订单”按钮
预期结果:提示支付跳转中



9、收货地址页面功能
测试场景一:填写符合要求格式的姓名,电话,省市区,详细地址,还有是否默认
预期结果:注册成功

测试场景二:编辑地址,修改电话,点击保存
预期结果:编辑成功

测试场景三:删除地址,点击“删除”按钮
预期结果:提示:删除后不可恢复、确认操作吗?点击确认,删除成功


10、用户中心页面功能
测试场景一:个人中心->个人资料,编辑个人资料,必填项:昵称:test123,非必填项:生日,所在省,所在市,所在区/县,详细地址,性别,点击保存
预期结果:编辑成功


测试场景二:个人中心->安全设置,登录密码修改,输入当前密码,新密码,确认密码,点击“保存”按钮
预期结果:提示:修改成功

测试场景三:个人中心->安全设置,登录密码修改,输入当前密码,新密码,确认密码,新密码与确认密码不一致。点击“保存”按钮
预期结果:提示确认密码与新密码不一致

测试场景四:个人中心->安全设置,登录密码修改,输入错误的当前密码,新密码,确认密码,点击“保存”按钮
预期结果:提示当前密码错误

测试场景五:个人中心->我的足迹,通过输入商品信息/销售价格/原价/浏览量/创建时间进行筛选,点击搜索图标进行搜索
预期结果:搜索成功

测试场景六:个人中心->我的足迹,单个商品删除,点击商品后面的“删除”按钮,
预期结果:提示:删除后不可恢复、确认操作吗?点击确认,删除成功


测试场景七:个人中心->我的足迹,点击“详情”按钮
预期结果:弹出商品详情弹窗

11、我的订单页面功能
测试场景一:订单支付,单个支付,点击商品后的“支付”按钮,弹出支付弹窗,选择支付方式,点击确认支付
预期结果:支付成功




实际结果:支付状态依旧是待支付
bug:选择"货到付款"提交订单后,订单列表显示状态为 "待支付",并展示了 "支付" 操作按钮。该流程与货到付款的业务逻辑不符。
测试场景二:订单详情,点击“详情”按钮
预期场景:跳转到订单详情页面


测试场景三:取消订单,点击“取消”按钮
预期场景:提示:操作后不可恢复、确认继续吗?点击确认,订单取消成功




测试场景四:搜索订单,通过输入基础信息/订单状态/支付状态/总价/支付金额/单价/订单模式/下单平台/地址信息/服务信息/取货信息/退款金额/退货数量/购买总数/增加金额/优惠金额/支付方式/留言信息/扩展信息/快递信息/是否评论/确认时间/支付时间/发货时间。完成时间/取消时间/关闭时间/创建时间/更新时间进行筛选,点击搜索图标进行搜索
预期场景:搜索成功

12、商品收藏页面功能
测试场景一:商品删除,单个删除,点击商品后面的“删除”按钮
预期场景:提示:操作后不可恢复、确认继续吗?点击确认,订单取消成功



测试场景二:点击“详情”按钮
预期场景:弹出商品详情弹窗

测试场景三:搜索商品,通过输入商品信息/销售价格/原价/创建时间进行筛选,点击搜索图标进行搜索
预期场景:搜索成功

13、我的消息页面功能
测试场景一:消息搜索,通过输入消息类型/业务类型/标题/详情/状态/时间进行筛选,点击搜索图标进行搜索
预期结果:搜索成功

测试场景二:点击“详情”按钮
预期结果:弹出商品详情弹窗

四、UI自动化测试
1、UI自动化测试项目代码文件结构
malltest/
├── pytest.ini # pytest 配置
├── requirements.txt # 依赖:pytest、selenium、allure-pytest等
├── README.md
│
├── config/ # 运行与环境配置
│ ├── config.yaml # 全局配置
│ ├── logging.yaml # logging 分级与按天分割规则
│ └── browsers.yaml # 浏览器驱动、启动参数、ChromeDriver 查找策略
│
├── data/ # YAML 数据驱动
│ ├── accounts.yaml # 测试账号(test123 / 123123 等)
│ ├── common.yaml # 公共测试数据(超时、通用关键词等)
│ ├── home.yaml # 首页用例数据
│ ├── search.yaml # 商品搜索页用例数据
│ ├── category.yaml # 商品分类页用例数据
│ ├── goods_detail.yaml # 商品详情页用例数据
│ ├── cart.yaml # 购物车页用例数据
│ ├── checkout.yaml # 订单结算页用例数据
│ ├── address.yaml # 收货地址用例数据
│ ├── order_confirm.yaml # 订单确认与提交用例数据
│ ├── login.yaml # 用户登录页用例数据
│ ├── register.yaml # 用户注册页用例数据
│ ├── user_center.yaml # 用户中心页用例数据
│ ├── order.yaml # 我的订单页用例数据
│ ├── favor.yaml # 商品收藏页用例数据
│ └── message.yaml # 我的消息页用例数据
│
├── pages/ # Page Object(按页面/模块划分)
│ ├── __init__.py
│ ├── base_page.py # 基类:公共操作、显式等待、登录弹窗(iframe)、多窗口切换、JS 增强等
│ ├── home_page.py # 首页
│ ├── search_page.py # 商品搜索页
│ ├── category_page.py # 商品分类页
│ ├── goods_detail_page.py # 商品详情页
│ ├── cart_page.py # 购物车页
│ ├── checkout_page.py # 订单结算页
│ ├── address_page.py # 收货地址
│ ├── order_confirm_page.py # 订单确认与提交
│ ├── login_page.py # 用户登录页
│ ├── register_page.py # 用户注册页
│ ├── user_center_page.py # 用户中心页
│ ├── order_page.py # 我的订单页
│ ├── favor_page.py # 商品收藏页
│ └── message_page.py # 我的消息页
│
├── utils/ # 工具与公共能力
│ ├── __init__.py
│ ├── driver_factory.py # WebDriver 创建与销毁
│ ├── logger.py # logging 封装(按天写入 TimedRotatingFileHandler)
│ ├── yaml_loader.py # YAML 读取与配置缓存
│ ├── wait_utils.py # 显式等待封装(visible/clickable/presence/...)
│ ├── ocr_utils.py # 验证码 OCR 识别
│ ├── restore.py # 登录态管理(会话级单次登录)+ 购物车数据还原
│ ├── screenshot_utils.py # 失败截图(按天目录归档)
│ └── exceptions.py # 自定义异常(MallTestError、ElementNotFoundError 等)
│
├── tests/ # pytest 测试用例(
│ ├── __init__.py
│ ├── conftest.py # fixture:driver、登录态、两阶段编排、购物车还原、失败截图钩子
│ ├── helpers.py # 跨用例辅助方法
│ │
│ ├── test_home.py # 一、首页
│ ├── test_search.py # 二、商品搜索页
│ ├── test_category.py # 三、商品分类页
│ ├── test_goods_detail.py # 四、商品详情页
│ ├── test_cart.py # 五、购物车页
│ ├── test_checkout.py # 六、订单结算页
│ ├── test_address.py # 七、收货地址
│ ├── test_order_confirm.py # 八、订单确认与提交
│ ├── test_login.py # 九、用户登录页
│ ├── test_register.py # 十、用户注册页
│ ├── test_user_center.py # 十一、用户中心页
│ ├── test_order.py # 十二、我的订单页
│ ├── test_favor.py # 十三、商品收藏页
│ └── test_message.py # 十四、我的消息页
│
├── logs/ # 运行日志(按天分割,运行时自动生成)
│
├── reports/ # 测试报告
│ ├── allure-results/ # Allure 原始结果(每次 pytest --clean-alluredir 自动清空)
│ └── allure-report/ # Allure HTML 报告(allure generate 后生成)
│
├── screenshots/ # 失败截图(按日期或用例名归档)
│
└── drivers/ # 浏览器驱动
└── .gitkeep
2、驱动类 (driver_factory.py)
import os
from pathlib import Path
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service
from utils.logger import setup_logging
from utils.yaml_loader import load_browser_config, load_config
logger = setup_logging()
def _as_bool(value, default: bool = False) -> bool:
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.strip().lower() in ("true", "1", "yes", "on")
return default
def _find_cached_chromedriver() -> Path | None:
"""优先使用 Selenium 已下载到本地的 chromedriver,避免启动时访问 Google 超时。"""
cache_roots = [
Path.home() / ".cache" / "selenium" / "chromedriver",
Path(os.environ.get("LOCALAPPDATA", "")) / "selenium" / "chromedriver",
]
candidates: list[Path] = []
for root in cache_roots:
if root.exists():
candidates.extend(root.rglob("chromedriver.exe"))
if not candidates:
return None
latest = max(candidates, key=lambda path: path.stat().st_mtime)
logger.info("使用 Selenium 本地缓存 ChromeDriver: %s", latest)
return latest
def _resolve_chromedriver() -> Path | None:
browser_cfg = load_browser_config()
project_driver = Path(__file__).resolve().parents[1] / browser_cfg.get("driver_path", "drivers/chromedriver.exe")
if project_driver.exists():
logger.info("使用项目内 ChromeDriver: %s", project_driver)
return project_driver
return _find_cached_chromedriver()
def create_driver():
cfg = load_config()
browser_cfg = load_browser_config()
headless = _as_bool(cfg.get("headless"), False)
options = Options()
options.page_load_strategy = cfg.get("page_load_strategy", "normal")
for arg in browser_cfg.get("chrome_options", []):
if headless or arg != "--window-size=1920,1080":
options.add_argument(arg)
if headless:
options.add_argument("--headless=new")
else:
options.add_argument("--start-maximized")
options.add_experimental_option("excludeSwitches", ["enable-automation"])
width = cfg.get("window_size", {}).get("width", 1920)
height = cfg.get("window_size", {}).get("height", 1080)
if headless:
options.add_argument(f"--window-size={width},{height}")
driver_path = _resolve_chromedriver()
if driver_path:
driver = webdriver.Chrome(service=Service(str(driver_path)), options=options)
else:
logger.warning(
"未找到本地 ChromeDriver,将尝试 Selenium 在线匹配(国内网络可能等待 60s+)。"
"建议将 chromedriver.exe 放入 drivers/ 目录。"
)
driver = webdriver.Chrome(options=options)
driver.set_page_load_timeout(cfg.get("page_load_timeout", 25))
if not headless:
try:
driver.maximize_window()
except Exception:
driver.set_window_size(width, height)
logger.info("ChromeDriver 已启动,headless=%s, page_load_strategy=%s", headless, options.page_load_strategy)
return driver
def quit_driver(driver):
if driver:
driver.quit()
logger.info("ChromeDriver 已关闭")
3、首页模块测试类 (test_home.py)
import allure
import pytest
from pages.home_page import HomePage
from utils.restore import logout
@allure.feature("首页")
class TestHomePage:
@pytest.mark.order(1)
@allure.story("首页加载与核心区域布局")
def test_home_layout_sections(self, driver):
page = HomePage(driver)
page.open_home()
assert page.is_element_visible(page.HEADER_TOP)
assert page.is_element_visible(page.NAV_SEARCH)
assert page.is_element_visible(page.SHOP_NAV)
assert page.is_element_visible(page.BANNER)
assert page.is_element_visible(page.GOODS_LIST)
assert page.is_element_visible(page.FOOTER)
assert page.is_element_visible(page.LOGIN_LINK)
assert page.is_element_visible(page.REGISTER_LINK)
@pytest.mark.order(2)
@allure.story("轮播图展示")
def test_home_banner(self, driver):
page = HomePage(driver)
page.open_home()
slides = page.get_visible_banner_slides()
assert len(slides) >= 1
assert slides[0].is_displayed()
@pytest.mark.order(3)
@allure.story("关键词搜索")
def test_home_search(self, driver, common_data):
page = HomePage(driver)
page.open_home()
page.search_keyword(common_data["search_keyword"])
assert "search/index" in driver.current_url
@pytest.mark.order(4)
@allure.story("搜索热词跳转")
def test_home_hot_keyword(self, driver):
page = HomePage(driver)
page.open_home()
page.click_first_hot_keyword()
assert "search/index" in driver.current_url
@pytest.mark.order(5)
@allure.story("商品楼层与卡片信息")
def test_home_floor_and_goods_card(self, driver):
page = HomePage(driver)
page.open_home()
titles = page.get_floor_titles()
assert titles
card = page.get_first_goods_card()
assert card is not None
assert page.is_element_visible(page.GOODS_PRICE)
@pytest.mark.order(6)
@allure.story("商品卡片进入详情")
def test_home_open_goods_detail(self, driver):
page = HomePage(driver)
page.open_home()
href = page.click_first_goods()
assert "goods/index" in driver.current_url or "goods/index" in href
@pytest.mark.order(7)
@allure.story("分类快捷入口")
def test_home_category_quick_link(self, driver):
page = HomePage(driver)
page.open_home()
page.click_first_category_quick()
assert "search/index" in driver.current_url
@pytest.mark.order(8)
@allure.story("全部分类入口")
def test_home_all_category(self, driver):
page = HomePage(driver)
page.open_home()
page.click_all_category()
assert "category/index" in driver.current_url
@pytest.mark.order(9)
@allure.story("未登录点击购物车弹出登录")
def test_home_cart_login_modal(self, driver):
# 关闭前面用例可能残留的新窗口
windows = driver.window_handles
if len(windows) > 1:
for w in windows[1:]:
driver.switch_to.window(w)
driver.close()
driver.switch_to.window(windows[0])
logout(driver)
page = HomePage(driver)
page.open_home()
page.click_cart_when_logged_out()
assert page.is_login_popup_visible()
page.close_login_popup_by_x()
assert not page.is_login_popup_visible()
page.click_cart_when_logged_out()
assert page.is_login_popup_visible()
page.close_login_popup_by_outside_click()
assert not page.is_login_popup_visible()
@pytest.mark.order(10)
@allure.story("资讯头条")
def test_home_news_section(self, driver):
page = HomePage(driver)
page.open_home()
assert page.is_element_visible(page.NEWS_TITLE)
assert len(driver.find_elements(*page.NEWS_ITEMS)) >= 1
@pytest.mark.order(11)
@allure.story("页脚链接")
@pytest.mark.parametrize(
"footer_title",
["信息咨询", "客户服务", "支付方式", "会员中心"],
)
def test_home_footer_group_links(self, driver, footer_title):
page = HomePage(driver)
page.open_home()
page.scroll_to_bottom()
titles = [el.text.strip() for el in driver.find_elements(*page.FOOTER_NAV_TITLE)]
assert footer_title in titles
assert page.is_element_visible(page.FOOTER_ABOUT)
assert page.is_element_visible(page.FOOTER_FILING)
4、登录模块测试类 (test_login.py)
import allure
import pytest
from pages.login_page import LoginPage
from pages.register_page import RegisterPage
from utils.restore import is_logged_in, logout
@allure.feature("用户登录")
class TestLoginPage:
@pytest.fixture(autouse=True)
def setup_logout(self, driver):
logout(driver)
@pytest.mark.order(1)
@allure.story("登录页布局与Tab")
def test_login_page_layout_and_tabs(self, driver):
page = LoginPage(driver)
page.open()
assert "欢迎登录" in driver.page_source
tabs = page.get_tab_texts()
assert "账号密码" in tabs
assert "手机验证码" in tabs
assert "邮箱验证码" in tabs
@pytest.mark.order(2)
@allure.story("正确账号登录")
def test_login_success(self, driver, account):
page = LoginPage(driver)
page.open()
assert page.login_with_captcha(account["username"], account["password"])
assert not page.is_on_login_page()
@pytest.mark.order(3)
@allure.story("错误密码登录")
def test_login_wrong_password(self, driver, account):
page = LoginPage(driver)
page.open()
assert not page.login_with_captcha(account["username"], "wrong_pass_999", max_retry=1)
assert page.is_on_login_page()
@pytest.mark.order(4)
@allure.story("必填项校验")
def test_login_required_validation(self, driver):
page = LoginPage(driver)
page.open()
page.submit_empty_form()
assert page.is_on_login_page()
@pytest.mark.order(5)
@allure.story("跳转注册与找回密码")
def test_login_navigation_links(self, driver):
page = LoginPage(driver)
page.open()
register = driver.find_element(*page.REGISTER_LINK)
assert "reginfo" in register.get_attribute("href")
forget = driver.find_element(*page.FORGET_LINK)
assert "forgetpwd" in forget.get_attribute("href")
5、注册模块测试类 (test_register.py)
import allure
import pytest
from pages.register_page import RegisterPage
from utils.restore import logout
@allure.feature("用户注册")
class TestRegisterPage:
@pytest.fixture(autouse=True)
def setup_logout(self, driver):
logout(driver)
@pytest.mark.order(1)
@allure.story("注册页布局与Tab")
def test_register_layout_and_tabs(self, driver):
page = RegisterPage(driver)
page.open_register()
assert "欢迎注册" in driver.page_source
tabs = page.get_tab_texts()
assert "账号注册" in tabs
assert "手机注册" in tabs
@pytest.mark.order(2)
@allure.story("必填校验")
def test_register_required_validation(self, driver):
page = RegisterPage(driver)
page.open_register()
page.submit_register()
assert "reginfo" in driver.current_url
@pytest.mark.order(3)
@allure.story("密码格式校验")
def test_register_password_format(self, driver):
page = RegisterPage(driver)
page.open_register()
page.submit_register(username="auto_test_user", password="123", fill_verify=True)
assert "reginfo" in driver.current_url
@pytest.mark.order(4)
@allure.story("验证码刷新")
def test_register_refresh_captcha(self, driver):
page = RegisterPage(driver)
page.open_register()
before = driver.find_element(*page.VERIFY_IMG).get_attribute("src")
page.refresh_captcha()
after = driver.find_element(*page.VERIFY_IMG).get_attribute("src")
assert before != after
@pytest.mark.order(5)
@allure.story("跳转登录")
def test_register_go_login(self, driver):
page = RegisterPage(driver)
page.open_register()
login_link = driver.find_element(*page.LOGIN_LINK)
assert "logininfo" in login_link.get_attribute("href")
6、搜索模块测试类 (test_search.py)
import allure
import pytest
from pages.search_page import SearchPage
@allure.feature("商品搜索页")
class TestSearchPage:
@pytest.mark.order(1)
@allure.story("分类搜索结果默认加载")
def test_search_default_load(self, logged_in_driver):
page = SearchPage(logged_in_driver)
page.open_phone_category()
assert page.is_element_visible(page.BODY)
assert page.get_goods_count() >= 1
@pytest.mark.order(2)
@allure.story("二次关键词搜索")
def test_search_second_keyword(self, logged_in_driver, common_data):
page = SearchPage(logged_in_driver)
page.open_phone_category()
page.search_keyword(common_data["search_keyword_second"])
assert "search/index" in logged_in_driver.current_url
@pytest.mark.order(3)
@allure.story("品牌筛选")
def test_search_brand_filter(self, logged_in_driver):
page = SearchPage(logged_in_driver)
page.open_phone_category()
if page.click_first_brand():
assert page.get_goods_count() >= 0
@pytest.mark.order(4)
@allure.story("价格预设筛选")
def test_search_price_preset(self, logged_in_driver):
page = SearchPage(logged_in_driver)
page.open_phone_category()
if page.click_first_price_preset():
assert "search/index" in logged_in_driver.current_url
@pytest.mark.order(5)
@allure.story("自定义价格区间")
def test_search_custom_price(self, logged_in_driver, common_data):
page = SearchPage(logged_in_driver)
page.open_laptop_category()
pf = common_data["price_filter"]
page.apply_custom_price(str(pf["min"]), str(pf["max"]))
assert page.get_goods_count() >= 0
@pytest.mark.order(6)
@allure.story("排序")
@pytest.mark.parametrize("sort_name", ["综合", "销量", "价格"])
def test_search_sort_options(self, logged_in_driver, sort_name):
page = SearchPage(logged_in_driver)
page.open_phone_category()
page.click_sort_by_text(sort_name)
assert page.is_element_visible(page.SORT_NAV)
@pytest.mark.order(7)
@allure.story("商品卡片信息")
def test_search_goods_card_info(self, logged_in_driver):
page = SearchPage(logged_in_driver)
page.open_phone_category()
assert page.is_element_visible(page.GOODS_IMAGES)
assert page.is_element_visible(page.GOODS_TITLE)
assert page.is_element_visible(page.GOODS_PRICE)
@pytest.mark.order(8)
@allure.story("进入商品详情")
def test_search_open_goods_detail(self, logged_in_driver):
page = SearchPage(logged_in_driver)
page.open_phone_category()
page.click_first_goods()
assert "goods/index" in logged_in_driver.current_url
@pytest.mark.order(9)
@allure.story("分页组件")
def test_search_pagination(self, logged_in_driver):
page = SearchPage(logged_in_driver)
page.open_phone_category()
assert page.is_element_visible(page.PAGINATION)
assert page.is_element_visible(page.PAGINATION_ACTIVE)
@pytest.mark.order(10)
@allure.story("无结果关键词")
def test_search_empty_result(self, logged_in_driver, common_data):
page = SearchPage(logged_in_driver)
page.open_home()
page.search_keyword(common_data["empty_result_keyword"])
assert page.get_goods_count() == 0 or page.is_element_visible(page.EMPTY_TIPS)
7、分类模块测试类 (test_category.py)
import allure
import pytest
from pages.category_page import CategoryPage
from pages.home_page import HomePage
from utils.restore import is_logged_in
@allure.feature("商品分类页")
class TestCategoryPage:
@pytest.mark.order(1)
@allure.story("分类页默认加载")
def test_category_default_load(self, logged_in_driver):
page = CategoryPage(logged_in_driver)
page.open_category()
assert page.is_element_visible(page.ALL_GOODS_TITLE)
@pytest.mark.order(2)
@allure.story("一级分类列表")
def test_category_level1_list(self, logged_in_driver):
page = CategoryPage(logged_in_driver)
page.open_category()
names = page.get_level1_names()
assert len(names) >= 3
@pytest.mark.order(3)
@allure.story("悬停展开子分类")
def test_category_hover_subcategory(self, logged_in_driver):
page = CategoryPage(logged_in_driver)
page.open_category()
page.hover_level1_by_index(0)
assert page.is_subcategory_panel_visible()
@pytest.mark.order(4)
@allure.story("三级分类跳转列表")
def test_category_level3_to_search(self, logged_in_driver):
page = CategoryPage(logged_in_driver)
page.open_category()
page.hover_level1_by_index(0)
page.click_level3_by_text("手机")
assert "search/index" in logged_in_driver.current_url
@pytest.mark.order(5)
@allure.story("顶部分类页搜索")
def test_category_search(self, logged_in_driver, common_data):
page = CategoryPage(logged_in_driver)
page.open_category()
page.search_keyword(common_data["search_keyword"])
assert "search/index" in logged_in_driver.current_url
@pytest.mark.order(6)
@allure.story("返回首页")
def test_category_back_home(self, logged_in_driver):
page = CategoryPage(logged_in_driver)
page.open_category()
page.back_to_home()
assert logged_in_driver.current_url.rstrip("/").endswith("49.235.61.184") or "index" in logged_in_driver.current_url
@pytest.mark.order(7)
@allure.story("已登录顶部信息")
def test_category_logged_in_header(self, logged_in_driver, account):
page = CategoryPage(logged_in_driver)
page.open_category()
assert is_logged_in(logged_in_driver)
@pytest.mark.order(8)
@allure.story("进入购物车")
def test_category_open_cart(self, logged_in_driver):
page = CategoryPage(logged_in_driver)
page.open_category()
page.driver.find_element(*page.CART_NAV).click()
assert "cart/index" in logged_in_driver.current_url
8、商品详情模块测试类 (test_goods_detail.py)
import allure
import pytest
from selenium.webdriver.common.by import By
from pages.goods_detail_page import GoodsDetailPage
from utils.restore import clear_cart_if_needed, is_logged_in, logout
@allure.feature("商品详情页")
class TestGoodsDetailPage:
@pytest.fixture(autouse=True)
def cleanup_cart(self, driver):
yield
if is_logged_in(driver):
clear_cart_if_needed(driver)
@pytest.mark.order(0)
@allure.story("未登录加购需登录")
def test_goods_add_cart_need_login(self, driver):
logout(driver)
page = GoodsDetailPage(driver)
page.open_goods()
page.driver.find_element(*page.CART_SUBMIT).click()
assert (
"login" in driver.current_url
or driver.find_elements(By.CSS_SELECTOR, ".am-popup.popup-iframe")
or driver.find_elements(By.CSS_SELECTOR, ".user-login-modal-container")
)
@pytest.mark.order(1)
@allure.story("详情页基础信息")
def test_goods_detail_basic_info(self, logged_in_driver):
page = GoodsDetailPage(logged_in_driver)
page.open_goods()
assert page.is_element_visible(page.DETAIL_ROOT)
assert page.get_price_text()
@pytest.mark.order(2)
@allure.story("Tab切换")
@pytest.mark.parametrize("tab_name", ["详情", "评价"])
def test_goods_detail_tabs(self, logged_in_driver, tab_name):
page = GoodsDetailPage(logged_in_driver)
page.open_goods()
page.switch_tab(tab_name)
assert page.is_element_visible(page.TAB_NAV)
@pytest.mark.order(3)
@allure.story("加入购物车")
def test_goods_add_to_cart(self, logged_in_driver):
page = GoodsDetailPage(logged_in_driver)
page.open_goods()
page.add_to_cart()
assert page.is_cart_success_shown()
page.continue_shopping()
@pytest.mark.order(4)
@allure.story("立即购买")
def test_goods_buy_now(self, logged_in_driver):
page = GoodsDetailPage(logged_in_driver)
page.open_goods()
page.buy_now()
assert "buy/index" in logged_in_driver.current_url or "login" in logged_in_driver.current_url
@pytest.mark.order(5)
@allure.story("收藏商品")
def test_goods_favor(self, logged_in_driver):
page = GoodsDetailPage(logged_in_driver)
page.open_goods()
page.click_favor()
assert "goods/index" in logged_in_driver.current_url
@pytest.mark.order(6)
@allure.story("详情页搜索")
def test_goods_detail_search(self, logged_in_driver, common_data):
page = GoodsDetailPage(logged_in_driver)
page.open_goods()
page.search_keyword(common_data["search_keyword"])
assert "search/index" in logged_in_driver.current_url
9、购物车模块测试类 (test_cart.py)
import allure
import pytest
from pages.cart_page import CartPage
from pages.goods_detail_page import GoodsDetailPage
from tests.helpers import prepare_cart_with_one_item
from utils.restore import clear_cart_if_needed
import time
@allure.feature("购物车页")
class TestCartPage:
@pytest.fixture(autouse=True)
def cleanup(self, logged_in_driver):
yield
clear_cart_if_needed(logged_in_driver)
@pytest.mark.order(1)
@allure.story("空购物车展示")
def test_cart_empty_state(self, logged_in_driver):
clear_cart_if_needed(logged_in_driver)
page = CartPage(logged_in_driver)
page.open_cart()
assert page.is_empty()
assert "空的" in page.get_empty_title()
@pytest.mark.order(2)
@allure.story("空购物车引导链接")
@pytest.mark.parametrize(
"link_locator,expected",
[
(CartPage.EMPTY_FAVOR_LINK, "usergoodsfavor"),
(CartPage.EMPTY_ORDER_LINK, "order/index"),
],
)
def test_cart_empty_guide_links(self, logged_in_driver, link_locator, expected):
clear_cart_if_needed(logged_in_driver)
page = CartPage(logged_in_driver)
page.open_cart()
logged_in_driver.find_element(*link_locator).click()
assert expected in logged_in_driver.current_url
@pytest.mark.order(3)
@allure.story("有商品列表展示")
def test_cart_with_items(self, logged_in_driver):
page = prepare_cart_with_one_item(logged_in_driver)
assert not page.is_empty()
assert page.get_item_count() >= 1
@pytest.mark.order(4)
@allure.story("勾选与全选")
def test_cart_select_and_select_all(self, logged_in_driver):
page = prepare_cart_with_one_item(logged_in_driver)
page.unselect_all_items()
page.select_first_item()
page.select_all()
@pytest.mark.order(5)
@allure.story("修改数量")
def test_cart_change_quantity(self, logged_in_driver):
page = prepare_cart_with_one_item(logged_in_driver)
before = page.get_cart_badge_text()
page.increase_first_item_qty()
page.open_cart()
after = page.get_cart_badge_text()
assert after >= before
@pytest.mark.order(6)
@allure.story("删除商品")
def test_cart_delete_item(self, logged_in_driver):
page = prepare_cart_with_one_item(logged_in_driver)
page.delete_first_item()
page.open_cart()
assert page.is_empty() or page.get_item_count() == 0
@pytest.mark.order(7)
@allure.story("未选商品结算")
def test_cart_settlement_without_selection(self, logged_in_driver):
page = prepare_cart_with_one_item(logged_in_driver)
page.unselect_all_items()
if logged_in_driver.find_elements(*page.SETTLEMENT_BTN):
logged_in_driver.find_element(*page.SETTLEMENT_BTN).click()
time.sleep(1)
assert "buy/index" not in logged_in_driver.current_url or page.is_empty()
@pytest.mark.order(8)
@allure.story("选中商品去结算")
def test_cart_settlement_with_selection(self, logged_in_driver):
page = prepare_cart_with_one_item(logged_in_driver)
page.select_all()
page.click_settlement()
assert "buy/index" in logged_in_driver.current_url
10、结算模块测试类 (test_checkout.py)
import allure
import pytest
from pages.checkout_page import CheckoutPage
from tests.helpers import prepare_cart_with_one_item
from utils.restore import clear_cart_if_needed
@allure.feature("订单结算页")
class TestCheckoutPage:
@pytest.fixture(autouse=True)
def cleanup(self, logged_in_driver):
yield
clear_cart_if_needed(logged_in_driver)
def _goto_buy(self, driver):
cart = prepare_cart_with_one_item(driver)
cart.select_all()
cart.click_settlement()
return CheckoutPage(driver)
@pytest.mark.order(1)
@allure.story("从购物车进入订单确认")
def test_checkout_enter_from_cart(self, logged_in_driver):
page = self._goto_buy(logged_in_driver)
assert page.is_on_buy_page()
@pytest.mark.order(2)
@allure.story("商品清单展示")
def test_checkout_goods_list(self, logged_in_driver):
page = self._goto_buy(logged_in_driver)
assert page.has_goods_table()
@pytest.mark.order(3)
@allure.story("Tab导航")
def test_checkout_tabs(self, logged_in_driver):
page = self._goto_buy(logged_in_driver)
assert page.is_element_visible(page.BUY_TABS)
@pytest.mark.order(4)
@allure.story("无地址占位")
def test_checkout_no_address_placeholder(self, logged_in_driver):
page = self._goto_buy(logged_in_driver)
assert page.is_element_visible(page.ADDRESS_SECTION)
@pytest.mark.order(5)
@allure.story("支付方式区域")
def test_checkout_payment_section(self, logged_in_driver):
page = self._goto_buy(logged_in_driver)
assert page.is_element_visible(page.PAYMENT_SECTION)
@pytest.mark.order(6)
@allure.story("未选地址提交订单")
def test_checkout_submit_without_address(self, logged_in_driver):
page = self._goto_buy(logged_in_driver)
addr_val = page.get_address_id_value()
if addr_val in ("-1", "0", "", None):
page.click_submit_order()
assert "buy/index" in logged_in_driver.current_url
@pytest.mark.order(7)
@allure.story("货到付款提交订单")
def test_checkout_submit_cod_order(self, logged_in_driver):
"""选择货到付款 → 提交订单 → 验证跳转离开结算页(订单生成)"""
page = self._goto_buy(logged_in_driver)
page.select_payment_by_name("货到付款")
page.submit_order_and_wait()
assert "buy/index" not in logged_in_driver.current_url
11、收货地址模块测试类 (test_address.py)
import allure
import pytest
from pages.address_page import AddressPage
from tests.helpers import prepare_cart_with_one_item
from utils.restore import clear_cart_if_needed
@allure.feature("收货地址")
class TestAddressPage:
@pytest.fixture(autouse=True)
def cleanup(self, logged_in_driver):
yield
clear_cart_if_needed(logged_in_driver)
def _goto_buy_with_address(self, driver):
cart = prepare_cart_with_one_item(driver)
cart.select_all()
cart.click_settlement()
return AddressPage(driver)
@pytest.mark.order(1)
@allure.story("地址列表展示")
def test_address_list_display(self, logged_in_driver):
page = self._goto_buy_with_address(logged_in_driver)
assert page.get_address_count() >= 0
@pytest.mark.order(2)
@allure.story("打开新增地址表单")
def test_address_open_add_form(self, logged_in_driver):
page = self._goto_buy_with_address(logged_in_driver)
if page.driver.find_elements(*page.ADD_BTN):
page.click_add_address()
assert page.driver.find_elements(*page.NAME_INPUT) or page.driver.find_elements(*page.POPUP)
@pytest.mark.order(3)
@allure.story("选择地址")
def test_address_select(self, logged_in_driver):
page = self._goto_buy_with_address(logged_in_driver)
if page.get_address_count() > 0:
page.select_address_by_index(0)
assert page.is_element_visible(page.CONTACT_SUMMARY) or page.get_address_count() >= 1
@pytest.mark.order(4)
@allure.story("编辑地址入口")
def test_address_edit_entry(self, logged_in_driver):
page = self._goto_buy_with_address(logged_in_driver)
if logged_in_driver.find_elements(*page.EDIT_LINK):
page.click_edit_first_address()
assert logged_in_driver.find_elements(*page.NAME_INPUT) or page.driver.find_elements(*page.POPUP)
@pytest.mark.order(5)
@allure.story("默认地址标识")
def test_address_default_badge(self, logged_in_driver):
page = self._goto_buy_with_address(logged_in_driver)
badges = logged_in_driver.find_elements(*page.DEFAULT_BADGE)
if badges:
assert "默认" in badges[0].text
12、订单确认模块测试类 (test_order_confirm.py)
import allure
import pytest
from pages.order_confirm_page import OrderConfirmPage
from tests.helpers import prepare_cart_with_one_item
from utils.restore import clear_cart_if_needed
@allure.feature("订单确认与提交")
class TestOrderConfirmPage:
@pytest.fixture(autouse=True)
def cleanup(self, logged_in_driver):
yield
clear_cart_if_needed(logged_in_driver)
def _goto_confirm(self, driver):
cart = prepare_cart_with_one_item(driver)
cart.select_all()
cart.click_settlement()
page = OrderConfirmPage(driver)
if page.get_address_count() if hasattr(page, "get_address_count") else 0:
pass
from pages.address_page import AddressPage
addr = AddressPage(driver)
if addr.get_address_count() > 0:
addr.select_address_by_index(0)
return page
@pytest.mark.order(1)
@allure.story("完整信息核对")
def test_order_confirm_info(self, logged_in_driver):
cart = prepare_cart_with_one_item(logged_in_driver)
cart.select_all()
cart.click_settlement()
page = OrderConfirmPage(logged_in_driver)
assert page.has_goods_table()
assert page.is_element_visible(page.PAYMENT_SECTION)
@pytest.mark.order(2)
@allure.story("实付款展示")
def test_order_confirm_actual_payment(self, logged_in_driver):
cart = prepare_cart_with_one_item(logged_in_driver)
cart.select_all()
cart.click_settlement()
page = OrderConfirmPage(logged_in_driver)
payment_text = page.get_actual_payment_text()
assert payment_text == "" or "实付款" in payment_text or page.is_element_visible(page.SUBMIT_ORDER)
@pytest.mark.order(3)
@allure.story("提交订单按钮")
def test_order_confirm_submit_button(self, logged_in_driver):
cart = prepare_cart_with_one_item(logged_in_driver)
cart.select_all()
cart.click_settlement()
page = OrderConfirmPage(logged_in_driver)
assert page.is_element_visible(page.SUBMIT_ORDER)
@pytest.mark.order(4)
@allure.story("返回购物车")
def test_order_confirm_back_cart(self, logged_in_driver):
cart = prepare_cart_with_one_item(logged_in_driver)
cart.select_all()
cart.click_settlement()
logged_in_driver.back()
assert "cart/index" in logged_in_driver.current_url or "buy/index" in logged_in_driver.current_url
@pytest.mark.order(5)
@allure.story("货到付款完整下单")
def test_order_confirm_submit_cod(self, logged_in_driver):
"""完整下单:加购→结算→选货到付款→提交→验证订单生成"""
cart = prepare_cart_with_one_item(logged_in_driver)
cart.select_all()
cart.click_settlement()
page = OrderConfirmPage(logged_in_driver)
page.select_payment_by_name("货到付款")
page.submit_order_and_wait()
assert "buy/index" not in logged_in_driver.current_url
13、用户中心模块测试类 (test_user_center.py)
import allure
import pytest
from pages.user_center_page import UserCenterPage
@allure.feature("用户中心")
class TestUserCenterPage:
@pytest.mark.order(1)
@allure.story("用户中心概览")
def test_user_center_overview(self, logged_in_driver):
page = UserCenterPage(logged_in_driver)
page.open_user_center()
assert page.is_element_visible(page.USER_MAIN)
stats = page.get_stat_labels()
assert any("订单" in s for s in stats)
@pytest.mark.order(2)
@allure.story("左侧菜单跳转")
@pytest.mark.parametrize(
"menu_text,expected_path",
[
("订单管理", "order/index"),
("商品收藏", "usergoodsfavor"),
("个人资料", "personal/index"),
("我的地址", "useraddress/index"),
],
)
def test_user_center_menu_navigation(self, logged_in_driver, menu_text, expected_path):
page = UserCenterPage(logged_in_driver)
page.open_user_center()
page.click_menu_by_text(menu_text)
assert expected_path in logged_in_driver.current_url
@pytest.mark.order(3)
@allure.story("修改资料入口")
def test_user_center_modify_profile(self, logged_in_driver):
page = UserCenterPage(logged_in_driver)
page.open_user_center()
link = logged_in_driver.find_element(*page.MODIFY_PROFILE)
assert "personal/index" in link.get_attribute("href")
@pytest.mark.order(4)
@allure.story("我的地址入口")
def test_user_center_my_address(self, logged_in_driver):
page = UserCenterPage(logged_in_driver)
page.open_user_center()
link = logged_in_driver.find_element(*page.MY_ADDRESS)
assert "useraddress/index" in link.get_attribute("href")
14、订单模块测试类 (test_order.py)
import allure
import pytest
from pages.order_page import OrderPage
@allure.feature("我的订单")
class TestOrderPage:
@pytest.mark.order(1)
@allure.story("订单列表加载")
def test_order_list_load(self, logged_in_driver):
page = OrderPage(logged_in_driver)
page.open_order()
assert "我的订单" in logged_in_driver.page_source
assert page.is_element_visible(page.ORDER_TABLE)
@pytest.mark.order(2)
@allure.story("按状态筛选")
def test_order_filter_by_status(self, logged_in_driver):
page = OrderPage(logged_in_driver)
page.open_order()
page.filter_by_status("待付款")
assert "order/index" in logged_in_driver.current_url
page.reset_search()
@pytest.mark.order(3)
@allure.story("关键字搜索")
def test_order_search_keyword(self, logged_in_driver, common_data):
page = OrderPage(logged_in_driver)
page.open_order()
page.search_by_keyword(common_data["search_keyword"])
assert "order/index" in logged_in_driver.current_url
page.reset_search()
@pytest.mark.order(4)
@allure.story("批量支付未选订单")
def test_order_batch_pay_without_select(self, logged_in_driver):
page = OrderPage(logged_in_driver)
page.open_order()
if logged_in_driver.find_elements(*page.BATCH_PAY):
page.click_batch_pay()
assert "order/index" in logged_in_driver.current_url
@pytest.mark.order(5)
@allure.story("分页区域")
def test_order_pagination(self, logged_in_driver):
page = OrderPage(logged_in_driver)
page.open_order()
assert page.is_element_visible(page.PAGINATION) or page.is_element_visible(page.ORDER_TABLE)
15、收藏模块测试类 (test_favor.py)
import allure
import pytest
from selenium.webdriver.common.by import By
from pages.favor_page import FavorPage
from pages.goods_detail_page import GoodsDetailPage
@allure.feature("商品收藏")
class TestFavorPage:
@pytest.mark.order(1)
@allure.story("收藏列表加载")
def test_favor_list_load(self, logged_in_driver):
page = FavorPage(logged_in_driver)
page.open_favor()
assert "商品收藏" in logged_in_driver.page_source
@pytest.mark.order(2)
@allure.story("从收藏进入详情")
def test_favor_open_goods_detail(self, logged_in_driver):
page = FavorPage(logged_in_driver)
page.open_favor()
links = logged_in_driver.find_elements(
By.CSS_SELECTOR, "table.form-table-data-list a[href*='goods/index']"
)
if links:
links[0].click()
assert "goods/index" in logged_in_driver.current_url
else:
detail = GoodsDetailPage(logged_in_driver)
detail.open_goods()
detail.click_favor()
page.open_favor()
assert "usergoodsfavor" in logged_in_driver.current_url
@pytest.mark.order(3)
@allure.story("删除收藏")
def test_favor_delete(self, logged_in_driver):
detail = GoodsDetailPage(logged_in_driver)
detail.open_goods()
detail.click_favor()
page = FavorPage(logged_in_driver)
page.open_favor()
if logged_in_driver.find_elements(*page.ROW_CHECKBOX):
page.select_first_row()
page.delete_selected()
assert "usergoodsfavor" in logged_in_driver.current_url
16、消息模块测试类 (test_message.py)
import allure
import pytest
from pages.message_page import MessagePage
@allure.feature("我的消息")
class TestMessagePage:
@pytest.mark.order(1)
@allure.story("消息列表加载")
def test_message_list_load(self, logged_in_driver):
page = MessagePage(logged_in_driver)
page.open_message()
assert "我的消息" in logged_in_driver.page_source
assert page.is_element_visible(page.TABLE)
@pytest.mark.order(2)
@allure.story("按阅读状态筛选")
@pytest.mark.parametrize("read_status", ["未读", "已读"])
def test_message_filter_read_status(self, logged_in_driver, read_status):
page = MessagePage(logged_in_driver)
page.open_message()
page.filter_by_read_status(read_status)
assert "message/index" in logged_in_driver.current_url
page.reset_search()
@pytest.mark.order(3)
@allure.story("分页区域")
def test_message_pagination(self, logged_in_driver):
page = MessagePage(logged_in_driver)
page.open_message()
assert page.is_element_visible(page.PAGINATION) or page.is_element_visible(page.TABLE)
17、测试配置与生命周期管理 (conftest.py)
import pytest
from utils.driver_factory import create_driver, quit_driver
from utils.logger import setup_logging
from utils.restore import clear_cart_if_needed, ensure_login, is_logged_in, logout
from utils.screenshot_utils import take_screenshot
from utils.yaml_loader import load_config, load_yaml
logger = setup_logging()
# 阶段一:未登录态(首页 / 登录 / 注册 / 未登录专项用例)
GUEST_FILE_ORDER = {
"test_home.py": 1,
"test_login.py": 2,
"test_register.py": 3,
}
# 位于需登录模块中、但必须在登录前执行的用例
GUEST_TEST_ORDER = {
"test_goods_add_cart_need_login": 4,
}
# 阶段二:登录后执行业务页面(顺序 100+)
AUTH_FILE_ORDER = {
"test_search.py": 1,
"test_category.py": 2,
"test_goods_detail.py": 3,
"test_cart.py": 4,
"test_checkout.py": 5,
"test_address.py": 6,
"test_order_confirm.py": 7,
"test_user_center.py": 8,
"test_order.py": 9,
"test_favor.py": 10,
"test_message.py": 11,
}
AUTH_PHASE_BASE = 100
def _method_order(item) -> int:
for mark in item.iter_markers("order"):
if mark.args:
return int(mark.args[0])
return 0
def _compute_test_order(item) -> int:
fname = item.fspath.basename
method_ord = _method_order(item)
if item.name in GUEST_TEST_ORDER:
return GUEST_TEST_ORDER[item.name] * 1000 + method_ord
if fname in GUEST_FILE_ORDER:
return GUEST_FILE_ORDER[fname] * 1000 + method_ord
if fname in AUTH_FILE_ORDER:
return AUTH_PHASE_BASE * 1000 + AUTH_FILE_ORDER[fname] * 100 + method_ord
return 999_000 + method_ord
def pytest_collection_modifyitems(session, config, items):
"""先跑未登录用例,再登录并跑需登录页面用例。"""
for item in items:
item.own_markers = [m for m in item.own_markers if m.name != "order"]
item.add_marker(pytest.mark.order(_compute_test_order(item)))
@pytest.fixture(scope="session")
def config():
return load_config()
@pytest.fixture(scope="session")
def account():
return load_yaml("data/accounts.yaml")["default"]
@pytest.fixture(scope="session")
def common_data():
return load_yaml("data/common.yaml")
@pytest.fixture(scope="session")
def driver():
"""整个测试会话共用一个浏览器窗口,便于观察执行过程。"""
drv = create_driver()
yield drv
quit_driver(drv)
@pytest.fixture(scope="session")
def logged_in_driver(driver, account):
"""阶段二首次需要登录的用例触发,会话内只登录一次。"""
ensure_login(driver, account["username"], account["password"])
logger.info("进入登录态测试阶段,Session 已就绪")
yield driver
@pytest.fixture(scope="class", autouse=True)
def cleanup_cart_after_module(request, driver):
"""购物车相关模块结束后统一还原,避免每个用例都清购物车。"""
module_name = getattr(request.module, "__name__", "")
cart_modules = {
"tests.test_goods_detail",
"tests.test_cart",
"tests.test_checkout",
"tests.test_address",
"tests.test_order_confirm",
}
yield
if module_name in cart_modules and is_logged_in(driver):
clear_cart_if_needed(driver)
@pytest.fixture
def logout_before(driver):
logout(driver)
yield driver
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
drv = item.funcargs.get("driver") or item.funcargs.get("logged_in_driver")
if drv:
take_screenshot(drv, item.name)
@pytest.fixture
def restore_cart(logged_in_driver):
yield
18、运行文件 (pytest.ini)
[pytest]
testpaths = tests
pythonpath = .
addopts = -v --alluredir=reports/allure-results --clean-alluredir --tb=short
markers =
order: 用例/模块执行顺序
filterwarnings =
ignore::DeprecationWarning
五、兼容性测试
测试场景一:在Chrome浏览器下进入电商购物平台
预期结果:UI界面显示正常,布局完整无错乱,与主流浏览器(如Edge、Firefox)表现无明显差异。




测试场景二:在Edge浏览器下进入电商购物平台
预期结果:UI界面显示正常,布局完整无错乱,与主流浏览器(如Chrome、Firefox)表现无明显差异。



测试场景三:在Firefox浏览器下进入电商购物平台
预期结果:UI界面显示正常,布局完整无错乱,与主流浏览器(如Chrome、Edge)表现无明显差异。




实际结果:UI界面显示正常,布局完整无错乱,与主流浏览器(如Chrome、Edge)对比,页面呈现比例较为紧凑。
bug:Firefox下功能正常,但整体布局比Chrome和Edge紧凑一些,像素级有偏差,需要前端调整一下兼容性。
六、接口测试
1、Postman配置
创建专门的测试集合用于博客系统测试
为每个测试页面(登录页、主页、博客详情页、博客编辑页)设置独立的请求
设置合理的请求头(Content-Type, User-Agent等)
2、用户注册接口

3、用户登录接口

4、商品评价接口

5、商品规格新增/编辑接口

6、加入购物车接口

7、购物车商品删除接口

8、下单/结算接口

七、接口自动化测试
接口自动化测试项目目录:
mallpost_api_autotest/
├── config/ # 配置文件目录
│ ├── config.yaml # 全局配置:base_url、超时、重试次数、路径
│ └── env.yaml # 环境配置
│
├── data/ # 测试数据目录(YAML 数据驱动)
│ ├── reg_data.yaml # 注册接口测试数据
│ ├── login_data.yaml # 登录接口测试数据
│ ├── comments_data.yaml # 商品评价测试数据
│ ├── spectype_data.yaml # 商品规格测试数据
│ ├── cart_save_data.yaml # 加入购物车测试数据
│ ├── cart_delete_data.yaml # 购物车删除测试数据
│ └── buy_add_data.yaml # 下单结算测试数据
│
├── schemas/ # jsonschema 响应校验文件目录
│ ├── reg_schema.json # 注册接口响应 schema
│ ├── login_schema.json # 登录接口响应 schema
│ ├── comments_schema.json # 商品评价响应 schema
│ ├── spectype_schema.json # 商品规格响应 schema
│ ├── cart_save_schema.json # 加入购物车响应 schema
│ ├── cart_delete_schema.json # 购物车删除响应 schema
│ └── buy_add_schema.json # 下单结算响应 schema
│
├── utils/ # 工具类目录
│ ├── __init__.py
│ ├── request_handler.py # requests.Session 封装(统一发送请求)
│ ├── captcha_helper.py # 验证码处理
│ ├── yaml_handler.py # YAML 文件读取
│ ├── schema_validator.py # jsonschema 校验封装
│ ├── logger.py # 日志配置:按天分割
│ ├── allure_helper.py # Allure 步骤/附件封装
│ └── base64_handler.py # 购物车
│
├── api/ # 接口定义层(每个业务模块一个文件)
│ ├── __init__.py
│ ├── base_api.py # 接口基类:持有 session、统一日志、统一异常
│ ├── user_api.py # 用户模块:注册 user/reg、登录 user/login
│ ├── goods_api.py # 商品模块:评价 goods/comments、规格 goods/spectype
│ ├── cart_api.py # 购物车模块:加入 cart/save、删除 cart/delete
│ └── buy_api.py # 下单模块:结算 buy/add
│
├── testcases/ # 测试用例层(pytest)
│ ├── __init__.py
│ ├── conftest.py # 局部 fixture:登录态、cart_id/goods_id 准备
│ ├── test_reg.py # 注册用例
│ ├── test_login.py # 登录用例
│ ├── test_comments.py # 商品评价用例
│ ├── test_spectype.py # 商品规格用例
│ ├── test_cart_save.py # 加入购物车用例
│ ├── test_cart_delete.py # 购物车删除用例
│ └── test_buy.py # 下单结算用例
│
├── logs/ # 日志输出目录(按天分割)
│
├── reports/ # Allure 报告目录
│ ├── allure-results/ # Allure 原始结果
│ └── allure-report/ # Allure 生成的最终报告
│
├── conftest.py # 全局 conftest:session fixture、全局前置/后置
├── pytest.ini # pytest 配置:标记、Allure 参数、用例路径
├── requirements.txt # 项目依赖
└── README.md # 项目说明
1、注册页接口测试类 (test_reg.py)
# -*- coding: utf-8 -*-
"""注册接口用例(REG-001 ~ REG-008)"""
import pytest
import allure
from utils.yaml_handler import load_data, build_params
cases = load_data("reg_data.yaml")
@allure.feature("注册接口")
class TestReg:
@pytest.mark.parametrize("case", build_params(cases))
def test_reg(self, case, user_api, resolve_markers, fill_captcha,
fresh_handler, assert_response, config):
allure.dynamic.story(case.get("priority", ""))
allure.dynamic.title(f"{case['case_id']} {case['desc']}")
payload = resolve_markers(case["payload"])
if case.get("need_captcha"):
payload = fill_captcha(payload)
if case.get("fresh_session"):
# 无 cookie 新会话(验证码过期场景)
path = config["api_paths"]["reg"]
resp = fresh_handler.post(path, data=payload)
elif case.get("pre_register"):
# 先注册一次(成功),再用新验证码重复注册(期望用户已存在)
first = fill_captcha(resolve_markers(case["payload"]))
user_api.register(**first)
again = fill_captcha(resolve_markers(case["payload"]))
resp = user_api.register(**again)
else:
resp = user_api.register(**payload)
assert_response(resp, case)
2、登录页接口测试类 (test_login.py)
# -*- coding: utf-8 -*-
"""登录接口用例(LOGIN-001 ~ LOGIN-008)"""
import pytest
import allure
from utils.yaml_handler import load_data, build_params
cases = load_data("login_data.yaml")
@allure.feature("登录接口")
class TestLogin:
@pytest.mark.parametrize("case", build_params(cases))
def test_login(self, case, user_api, resolve_markers, fill_captcha,
assert_response):
allure.dynamic.story(case.get("priority", ""))
allure.dynamic.title(f"{case['case_id']} {case['desc']}")
payload = resolve_markers(case["payload"])
if case.get("pre_register"):
# 先注册(user_reg 验证码),再登录(user_login 验证码)
# register_pwd 优先(用于注册时用正确密码,登录时用 payload.pwd 触发密码错误/格式错误)
if case.get("register_pwd"):
reg_pwd = resolve_markers({"_": case["register_pwd"]})["_"]
reg_dict = dict(payload)
reg_dict["pwd"] = reg_pwd
else:
reg_dict = dict(payload)
reg_payload = fill_captcha(reg_dict, "user_reg")
user_api.register(**reg_payload)
login_payload = fill_captcha(payload, "user_login")
resp = user_api.login(**login_payload)
else:
if case.get("need_captcha"):
payload = fill_captcha(payload, "user_login")
resp = user_api.login(**payload)
assert_response(resp, case)
3、商品评价接口测试类 (test_comments.py)
# -*- coding: utf-8 -*-
"""商品评价接口用例(CMT-001 ~ CMT-005)"""
import pytest
import allure
from utils.yaml_handler import load_data, build_params
cases = load_data("comments_data.yaml")
@allure.feature("商品评价接口")
class TestComments:
@pytest.mark.parametrize("case", build_params(cases))
def test_comments(self, case, goods_api, assert_response):
allure.dynamic.story(case.get("priority", ""))
allure.dynamic.title(f"{case['case_id']} {case['desc']}")
payload = case["payload"]
resp = goods_api.get_comments(
goods_id=payload["goods_id"], page=payload["page"]
)
assert_response(resp, case)
4、商品规格新增/编辑接口测试类 (test_spectype.py)
# -*- coding: utf-8 -*-
"""商品规格新增/编辑接口用例(SPEC-001 ~ SPEC-008)"""
import pytest
import allure
from utils.yaml_handler import load_data, build_params
cases = load_data("spectype_data.yaml")
@allure.feature("商品规格接口")
class TestSpectype:
@pytest.mark.parametrize("case", build_params(cases))
def test_spectype(self, case, goods_api, registered_user,
fresh_handler, assert_response, config):
allure.dynamic.story(case.get("priority", ""))
allure.dynamic.title(f"{case['case_id']} {case['desc']}")
payload = dict(case["payload"])
if case.get("no_auth"):
# 未授权:使用无 cookie 的新会话
path = config["api_paths"]["spectype"]
resp = fresh_handler.post(path, data=payload)
else:
resp = goods_api.save_spectype(payload)
assert_response(resp, case)
5、加入购物车接口测试类 (test_cart_save.py)
# -*- coding: utf-8 -*-
"""加入购物车接口用例(CART-001 ~ CART-008)"""
import pytest
import allure
from utils.yaml_handler import load_data, build_params
from utils.base64_handler import build_goods_data, encode_goods_data
cases = load_data("cart_save_data.yaml")
def _build_goods_data(raw):
"""根据 raw_goods 模式构造 goods_data"""
mode = raw.get("mode", "normal")
if mode == "normal":
return build_goods_data(raw["goods_id"], raw["stock"], raw.get("spec", []))
if mode == "not_exist_goods_id":
return build_goods_data(raw["goods_id"], raw["stock"], raw.get("spec", []))
if mode == "empty_goods_id":
return ""
if mode == "illegal_base64":
return "!!!不是base64!!!"
if mode == "malicious_json":
return encode_goods_data([{"goods_id": 2, "$where": "1==1"}])
return ""
@allure.feature("加入购物车接口")
class TestCartSave:
@pytest.mark.parametrize("case", build_params(cases))
def test_cart_save(self, case, cart_api, registered_user,
fresh_handler, assert_response, config):
allure.dynamic.story(case.get("priority", ""))
allure.dynamic.title(f"{case['case_id']} {case['desc']}")
goods_data = _build_goods_data(case["raw_goods"])
data = {"goods_data": goods_data}
if case.get("no_auth"):
path = config["api_paths"]["cart_save"]
resp = fresh_handler.post(path, data=data)
else:
resp = cart_api.add_to_cart(goods_data)
assert_response(resp, case)
6、购物车商品删除接口测试类 (test_cart_delete.py)
# -*- coding: utf-8 -*-
"""购物车删除接口用例(DEL-001 ~ DEL-008)"""
import pytest
import allure
from utils.yaml_handler import load_data, build_params
cases = load_data("cart_delete_data.yaml")
@allure.feature("购物车删除接口")
class TestCartDelete:
@pytest.mark.parametrize("case", build_params(cases))
def test_cart_delete(self, case, cart_api, cart_id,
resolve_markers, assert_response):
allure.dynamic.story(case.get("priority", ""))
allure.dynamic.title(f"{case['case_id']} {case['desc']}")
payload = resolve_markers(case["payload"], {"$cart_id": cart_id})
resp = cart_api.delete_cart(
cart_id=payload["id"], value=payload["value"], field=payload["field"]
)
assert_response(resp, case)
7、下单/结算接口测试类 (test_buy.py)
# -*- coding: utf-8 -*-
"""下单/结算接口用例(BUY-001 ~ BUY-004)"""
import pytest
import allure
from utils.yaml_handler import load_data, build_params
cases = load_data("buy_add_data.yaml")
@allure.feature("下单/结算接口")
class TestBuy:
@pytest.mark.parametrize("case", build_params(cases))
def test_buy(self, case, buy_api, cart_id, checkout_context,
resolve_markers, assert_response):
allure.dynamic.story(case.get("priority", ""))
allure.dynamic.title(f"{case['case_id']} {case['desc']}")
ctx = {
"$cart_id": cart_id,
"$address_id": checkout_context["address_id"],
"$payment_id": checkout_context["payment_id"],
}
payload = resolve_markers(case["payload"], ctx)
resp = buy_api.submit_order(
ids=payload["ids"],
address_id=payload["address_id"],
payment_id=payload["payment_id"],
user_note=payload.get("user_note", ""),
site_model=payload.get("site_model", 0),
appoint_time=payload.get("appoint_time", ""),
extraction_contact_name=payload.get("extraction_contact_name", ""),
extraction_contact_tel=payload.get("extraction_contact_tel", ""),
)
assert_response(resp, case)
八、总结-bug描述
1、商品详情页页面展示风格不统一



2、地址新增和编辑功能中对留存号码的鉴定不严谨
只校验了电话号码是否为空,未校验电话号码的位数和格式
提示词有误,电话号码为空的时候,提示电话格式有误,实则只校验了是否为空

对位数和格式均为做校验

3、订单支付模块逻辑有误
选择"货到付款"提交订单后,订单列表显示状态为 "待支付",并展示了 "支付" 操作按钮。该流程与货到付款的业务逻辑不符。




4、浏览器兼容性问题
Firefox下功能正常,但整体布局比Chrome和Edge挤一些,像素级有偏差,需要前端调整一下兼容性。
Chrome浏览器下:

Edge浏览器下:

Firefox浏览器下:

5、注册接口后端未校验密码长度,与前端规则不一致
注册页面前端限制密码长度为6-18位,但后端接口未做相同校验。通过接口直接传入5位密码,仍能注册成功。

更多推荐


所有评论(0)