一、报告概述

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位密码,仍能注册成功。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐