【Web UI 自动化】08 - 企业选型·报告·CI/CD

Web UI 自动化篇最后一篇。本篇完成 Allure 报告的完整集成、CI/CD 流水线配置、自动化测试策略文档,以及整个 Web UI 篇的总结回顾。


前言

框架搭好了,用例写好了,三种模式都有了。接下来要解决企业落地的最后三个问题:

  1. 报告怎么看:Allure 报告如何自定义分类、环境信息、趋势图
  2. 怎么自动跑:GitHub Actions / Jenkins Pipeline 配置
  3. 什么该自动化:自动化测试策略文档

一、Allure 报告完整集成

1.1 创建 conftest 的 Allure 元数据

在项目根目录创建 allure_config.py

python 复制代码
"""
Allure 报告配置
自定义分类、环境信息、执行器信息
"""

import json
import platform
from pathlib import Path
from datetime import datetime


REPORT_DIR = Path("reports/allure-results")
REPORT_DIR.mkdir(parents=True, exist_ok=True)


def write_categories():
    """
    自定义缺陷分类
    Allure 报告首页会按此分类统计失败原因
    """
    categories = [
        {
            "name": "元素定位失败",
            "matchedStatuses": ["failed"],
            "messageRegex": ".*(?:locator|selector|element|定位).*"
        },
        {
            "name": "断言失败",
            "matchedStatuses": ["failed"],
            "messageRegex": ".*(?:assert|AssertionError|断言|期望).*"
        },
        {
            "name": "超时失败",
            "matchedStatuses": ["failed"],
            "messageRegex": ".*(?:timeout|Timeout|超时|timed out).*"
        },
        {
            "name": "页面加载失败",
            "matchedStatuses": ["failed"],
            "messageRegex": ".*(?:Navigation|load|加载|net::ERR).*"
        },
        {
            "name": "系统错误",
            "matchedStatuses": ["broken"],
            "messageRegex": ".*(?:Exception|Error|Traceback).*"
        },
        {
            "name": "未知失败",
            "matchedStatuses": ["failed"]
        }
    ]
    filepath = REPORT_DIR / "categories.json"
    filepath.write_text(json.dumps(categories, ensure_ascii=False, indent=2))
    print(f"Allure 分类配置已写入:{filepath}")


def write_environment():
    """
    环境信息
    Allure 报告的 Environment 标签页会显示此信息
    """
    env_info = [
        {"name": "Project", "value": "MallLite UI Automation"},
        {"name": "Python", "value": platform.python_version()},
        {"name": "OS", "value": f"{platform.system()} {platform.release()}"},
        {"name": "Browser", "value": "Chromium"},
        {"name": "Playwright", "value": _get_playwright_version()},
        {"name": "Pytest", "value": _get_pytest_version()},
        {"name": "Base URL", "value": "http://localhost:8000"},
        {"name": "Environment", "value": "dev"},
        {"name": "Run Time", "value": datetime.now().strftime("%Y-%m-%d %H:%M:%S")},
        {"name": "Executor", "value": platform.node()},
    ]
    filepath = REPORT_DIR / "environment.properties"
    lines = [f"{item['name']}={item['value']}" for item in env_info]
    filepath.write_text("\n".join(lines))
    print(f"Allure 环境信息已写入:{filepath}")


def write_executor():
    """
    执行器信息
    支持 CI/CD 环境自动识别
    """
    import os
    executor = {
        "name": "MallLite UI Automation",
        "type": "pytest",
        "buildOrder": int(datetime.now().timestamp()),
        "buildName": f"UI Test {datetime.now().strftime('%Y%m%d_%H%M')}",
    }

    # GitHub Actions 环境
    if os.environ.get("GITHUB_ACTIONS"):
        executor["name"] = "GitHub Actions"
        executor["buildName"] = os.environ.get("GITHUB_WORKFLOW", "CI")
        executor["buildOrder"] = int(os.environ.get("GITHUB_RUN_NUMBER", 0))
        executor["reportName"] = "MallLite UI Test Report"

    # Jenkins 环境
    elif os.environ.get("JENKINS_URL"):
        executor["name"] = "Jenkins"
        executor["buildName"] = os.environ.get("JOB_NAME", "UI-Test")
        executor["buildOrder"] = int(os.environ.get("BUILD_NUMBER", 0))

    filepath = REPORT_DIR / "executor.json"
    filepath.write_text(json.dumps(executor, ensure_ascii=False, indent=2))
    print(f"Allure 执行器信息已写入:{filepath}")


def _get_playwright_version():
    try:
        import playwright
        return playwright.__version__
    except Exception:
        return "unknown"


def _get_pytest_version():
    try:
        import pytest
        return pytest.__version__
    except Exception:
        return "unknown"


if __name__ == "__main__":
    write_categories()
    write_environment()
    write_executor()
    print("Allure 配置文件生成完成")

1.2 更新 conftest.py 集成 Allure 配置

conftest.py 中追加会话级 hook:

python 复制代码
def pytest_sessionstart(session):
    """测试会话开始时生成 Allure 配置文件"""
    try:
        from allure_config import write_categories, write_environment, write_executor
        write_categories()
        write_environment()
        write_executor()
    except Exception as e:
        logger.warning(f"生成 Allure 配置失败:{e}")

1.3 Allure 报告自定义标签

在测试用例中使用 Allure 的各种标签,让报告更有结构:

python 复制代码
import allure

@allure.epic("MallLite 电商系统")
@allure.feature("登录功能")
class TestLogin:

    @allure.story("正常登录")
    @allure.severity(allure.severity_level.BLOCKER)
    @allure.title("管理员登录成功")
    @allure.description("使用管理员账号密码验证登录功能")
    @allure.link("http://localhost:8000/login", name="登录页面")
    @allure.issue("JIRA-1234", "登录功能需求")
    @allure.testcase("TC-001", "登录用例")
    def test_admin_login(self, login_page, admin_user):
        login_page.login(admin_user["username"], admin_user["password"])
        assert login_page.is_login_success()

Allure 报告中的展示层级:

复制代码
Epic: MallLite 电商系统
  └── Feature: 登录功能
        └── Story: 正常登录
              ├── TC-001: 管理员登录成功  [BLOCKER] ✅
              ├── TC-002: 普通用户登录成功 [BLOCKER] ✅
              └── TC-003: VIP用户登录成功  [BLOCKER] ✅

1.4 Allure 报告核心页面说明

复制代码
Allure 报告页面:

Overview(总览)
  ├── 统计面板:通过/失败/跳过/破损的数量和百分比
  ├── 趋势图:历史多次运行的通过率变化
  ├── 环境信息:来自 environment.properties
  ├── 缺陷分类:来自 categories.json(按失败原因分组)
  └── 严重级别分布:Blocker/Critical/Normal/Minor/Trivial

Behaviors(行为)
  ├── 按 Epic → Feature → Story 三层组织
  └── 每个 Story 下展示对应的测试用例

Packages(包)
  ├── 按 Python 模块/类组织
  └── 类似代码目录结构

Graphs(图表)
  ├── 严重级别分布饼图
  ├── 状态分布饼图
  ├── 持续时间 TOP 10
  └── 通过率趋势

Timeline(时间线)
  └── 按执行时间轴展示,可看到并行执行情况

Categories(分类)
  └── 按失败原因分类:元素定位失败/断言失败/超时失败/...

1.5 生成和查看报告

bash 复制代码
# 方式一:两步操作
# 第一步:运行测试生成数据
pytest -v --alluredir=reports/allure-results --clean-alluredir
# 第二步:生成报告并打开
allure serve reports/allure-results

# 方式二:用 run.py
python run.py report

# 方式三:生成静态报告(用于分享)
pytest -v --alluredir=reports/allure-results --clean-alluredir
allure generate reports/allure-results -o reports/allure-report --clean
# 打开 reports/allure-report/index.html

二、GitHub Actions CI/CD

2.1 创建 Workflow 配置

bash 复制代码
mkdir -p .github/workflows

创建 .github/workflows/ui-test.yml

yaml 复制代码
# MallLite UI 自动化测试 CI/CD
# 触发条件:push 到 main 分支、PR、手动触发、定时触发

name: UI Automation Test

on:
  push:
    branches: [main, develop]
    paths:
      - 'web_ui/**'
      - 'mall-lite/**'
  pull_request:
    branches: [main]
  schedule:
    # 每天凌晨 2 点运行冒烟测试
    - cron: '0 18 * * *'
  workflow_dispatch:
    inputs:
      test_suite:
        description: '测试套件'
        required: true
        default: 'smoke'
        type: choice
        options:
          - smoke
          - regression
          - all
      environment:
        description: '测试环境'
        required: true
        default: 'dev'
        type: choice
        options:
          - dev
          - staging

env:
  PYTHON_VERSION: '3.12'

jobs:
  ui-test:
    name: UI Test (${{ github.event.inputs.test_suite || 'smoke' }})
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      # 1. 检出代码
      - name: Checkout
        uses: actions/checkout@v4

      # 2. 配置 Python
      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: ${{ env.PYTHON_VERSION }}
          cache: 'pip'

      # 3. 安装依赖
      - name: Install Dependencies
        run: |
          cd web_ui
          pip install -r requirements.txt
          playwright install chromium
          playwright install-deps chromium

      # 4. 启动被测系统
      - name: Start MallLite
        run: |
          cd mall-lite
          pip install -r requirements.txt
          python run.py &
          sleep 5
          # 验证服务启动
          curl -s http://localhost:8000/ | head -5

      # 5. 运行测试
      - name: Run Tests
        run: |
          cd web_ui
          TEST_SUITE=${{ github.event.inputs.test_suite || 'smoke' }}
          ENV=${{ github.event.inputs.environment || 'dev' }}

          if [ "$TEST_SUITE" = "smoke" ]; then
            pytest -v -m smoke --headed=false --env=$ENV \
              --alluredir=reports/allure-results --clean-alluredir
          elif [ "$TEST_SUITE" = "regression" ]; then
            pytest -v -m regression --headed=false --env=$ENV \
              --alluredir=reports/allure-results --clean-alluredir
          else
            pytest -v --headed=false --env=$ENV \
              --alluredir=reports/allure-results --clean-alluredir
          fi
        env:
          PYTHONDONTWRITEBYTECODE: 1

      # 6. 上传 Allure 数据
      - name: Upload Allure Results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: allure-results
          path: web_ui/reports/allure-results/
          retention-days: 30

      # 7. 上传失败截图
      - name: Upload Screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: failure-screenshots
          path: web_ui/reports/screenshots/
          retention-days: 7

      # 8. 上传测试日志
      - name: Upload Test Logs
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: test-logs
          path: web_ui/reports/logs/
          retention-days: 7

      # 9. 生成 Allure 报告
      - name: Generate Allure Report
        if: always()
        uses: simple-elf/allure-report-action@master
        with:
          allure_results: web_ui/reports/allure-results

      # 10. 部署报告到 GitHub Pages
      - name: Deploy Report to GitHub Pages
        if: always()
        uses: peaceiris/actions-gh-pages@v3
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: allure-history

2.2 Workflow 说明

复制代码
触发条件:
  ├── push 到 main/develop 且改动了 web_ui 或 mall-lite → 自动运行
  ├── Pull Request → 自动运行
  ├── 每天凌晨 2 点(北京时间) → 自动运行冒烟测试
  └── 手动触发 → 选择测试套件和环境

执行流程:
  1. 检出代码
  2. 安装 Python + 依赖 + Playwright 浏览器
  3. 启动 MallLite 被测系统
  4. 运行测试(无头模式)
  5. 上传报告和截图(无论成功失败)
  6. 生成 Allure 报告
  7. 部署到 GitHub Pages

三、Jenkins Pipeline

创建 Jenkinsfile

groovy 复制代码
// MallLite UI 自动化测试 Jenkins Pipeline

pipeline {
    agent any

    parameters {
        choice(
            name: 'TEST_SUITE',
            choices: ['smoke', 'regression', 'all'],
            description: '选择测试套件'
        )
        choice(
            name: 'ENVIRONMENT',
            choices: ['dev', 'staging'],
            description: '选择测试环境'
        )
    }

    triggers {
        // 每天凌晨 2 点运行冒烟测试
        cron('0 18 * * *')
    }

    environment {
        PYTHON_VERSION = '3.12'
        BASE_URL = 'http://localhost:8000'
    }

    options {
        timeout(time: 30, unit: 'MINUTES')
        timestamps()
        ansiColor('xterm')
    }

    stages {
        stage('准备环境') {
            steps {
                sh '''
                    cd web_ui
                    python3 -m venv venv
                    source venv/bin/activate
                    pip install -r requirements.txt
                    playwright install chromium
                    playwright install-deps chromium
                '''
            }
        }

        stage('启动被测系统') {
            steps {
                sh '''
                    cd mall-lite
                    nohup python run.py > /tmp/malllite.log 2>&1 &
                    sleep 5
                    curl -sf http://localhost:8000/ || (echo "启动失败" && exit 1)
                '''
            }
        }

        stage('运行测试') {
            steps {
                sh """
                    cd web_ui
                    source venv/bin/activate
                    MARKER_OPT=""
                    if [ "${params.TEST_SUITE}" = "smoke" ]; then
                        MARKER_OPT="-m smoke"
                    elif [ "${params.TEST_SUITE}" = "regression" ]; then
                        MARKER_OPT="-m regression"
                    fi

                    pytest -v \$MARKER_OPT \\
                        --headed=false \\
                        --env=${params.ENVIRONMENT} \\
                        --alluredir=reports/allure-results \\
                        --clean-alluredir \\
                        --junitxml=reports/junit.xml \\
                        || true
                """
            }
        }
    }

    post {
        always {
            // 发布 JUnit 报告
            junit allowEmptyResults: true, testResults: 'web_ui/reports/junit.xml'

            // 发布 Allure 报告
            allure([
                includeProperties: false,
                results: [[path: 'web_ui/reports/allure-results']]
            ])

            // 归档截图
            archiveArtifacts allowEmptyArchive: true,
                artifacts: 'web_ui/reports/screenshots/**'

            // 归档日志
            archiveArtifacts allowEmptyArchive: true,
                artifacts: 'web_ui/reports/logs/**'
        }

        failure {
            echo '测试失败!请检查 Allure 报告和截图。'
        }

        success {
            echo '测试全部通过!'
        }

        cleanup {
            // 清理被测系统
            sh 'pkill -f "python run.py" || true'
        }
    }
}

四、自动化测试策略文档

4.1 什么适合自动化

复制代码
适合自动化 ✅:
  ├── 高频执行的冒烟测试(每次发布前必跑)
  ├── 核心业务链路(登录→搜索→下单)
  ├── 回归测试(确保老功能没被破坏)
  ├── 数据驱动测试(同一逻辑多组数据)
  ├── 跨浏览器兼容性测试
  └── 接口和 UI 联合测试

不适合自动化 ❌:
  ├── 一次性测试(只测一次的功能)
  ├── 探索性测试(需要人脑判断)
  ├── UI 视觉验证(样式、颜色、布局)
  ├── 涉及第三方支付的真实交易
  ├── 需要物理设备操作的场景
  └── 需求频繁变更、尚未稳定的功能

4.2 用例分层金字塔

复制代码
                    ┌──────────────┐
                   /│   E2E (UI)    │\         10%  ← POM 模式
                  / │  约 10-15 条   │ \            核心链路
                 /  └──────────────┘  \
                /   ┌──────────────┐   \
               /    │  接口测试     │    \     20%  ← API 自动化
              /     │  约 30-50 条  │     \
             /      └──────────────┘      \
            /    ┌──────────────────┐      \
           /     │   单元测试        │       \  70%  ← 开发自测
          /      │   约 100-200 条  │        \
         /       └──────────────────┘         \
        └────────────────────────────────────────┘

投入比例:单元测试 > 接口测试 > UI 测试
回报比例:单元测试回报最高,UI 测试成本最高

4.3 自动化用例优先级

优先级 标记 说明 执行频率 模式
P0 smoke + p0 核心冒烟,发布前必跑 每次提交 POM
P1 regression + p1 功能回归 每天定时 KDT + DDT
P2 regression + p2 边界和兼容性 每周 KDT + DDT
验收 bdd 产品验收测试 迭代结束 BDD

4.4 执行策略

bash 复制代码
# 开发提交代码时:运行冒烟测试(约 2 分钟)
pytest -v -m smoke --headed=false

# 每天凌晨:运行全量回归(约 10 分钟)
pytest -v -m regression --headed=false

# 每周:运行全部用例(POM + KDT + BDD)约 15 分钟
pytest -v --headed=false
behave test_cases/bdd/features/

# 发布前:运行 P0 冒烟 + BDD 验收
pytest -v -m "smoke and p0" --headed=false
behave test_cases/bdd/features/ --tags=@smoke

五、更新 run.py 最终版

python 复制代码
"""
MallLite UI 自动化测试 - 一键运行脚本(最终版)

用法:
    python run.py smoke            # POM 冒烟
    python run.py regression       # POM 回归
    python run.py login            # 登录模块
    python run.py search           # 搜索模块
    python run.py cart             # 购物车模块
    python run.py order            # 订单模块
    python run.py e2e              # 端到端
    python run.py kdt              # KDT 全部
    python run.py kdt-login        # KDT 登录
    python run.py ddt              # DDT 全部
    python run.py bdd              # BDD 全部
    python run.py bdd-smoke        # BDD 冒烟
    python run.py all              # 全部(POM + KDT)
    python run.py report           # 生成 Allure 报告
    python run.py bdd-report       # 生成 BDD Allure 报告
    python run.py headless         # 无头模式全量
    python run.py setup            # 生成 Allure 配置文件
"""

import subprocess
import sys


def run(cmd):
    print(f"\n{'=' * 60}")
    print(f"  执行:{' '.join(cmd)}")
    print(f"{'=' * 60}\n")
    return subprocess.run(cmd, cwd=".", shell=True).returncode


def pytest_cmd(*args):
    return run([sys.executable, "-m", "pytest", "-v", *args])


def main():
    if len(sys.argv) < 2:
        print(__doc__)
        return

    cmd = sys.argv[1].lower()

    # POM 模式
    pom_modules = {
        "smoke": lambda: pytest_cmd("-m", "smoke"),
        "regression": lambda: pytest_cmd("-m", "regression"),
        "login": lambda: pytest_cmd("-m", "login"),
        "search": lambda: pytest_cmd("-m", "search"),
        "product": lambda: pytest_cmd("-m", "product"),
        "cart": lambda: pytest_cmd("-m", "cart"),
        "order": lambda: pytest_cmd("-m", "order"),
        "e2e": lambda: pytest_cmd("-m", "e2e"),
        "p0": lambda: pytest_cmd("-m", "p0"),
    }

    # KDT 模式
    kdt_commands = {
        "kdt": lambda: pytest_cmd("test_cases/kdt/"),
        "kdt-login": lambda: pytest_cmd("test_cases/kdt/", "-k", "login"),
        "kdt-search": lambda: pytest_cmd("test_cases/kdt/", "-k", "search"),
        "kdt-cart": lambda: pytest_cmd("test_cases/kdt/", "-k", "cart"),
        "ddt": lambda: pytest_cmd("test_cases/kdt/", "-k", "ddt"),
    }

    # BDD 模式
    def behave_cmd(*args):
        return run(["behave", "test_cases/bdd/features/", *args])

    bdd_commands = {
        "bdd": lambda: behave_cmd(),
        "bdd-smoke": lambda: behave_cmd("--tags=@smoke"),
        "bdd-login": lambda: run(["behave", "test_cases/bdd/features/login.feature"]),
        "bdd-search": lambda: run(["behave", "test_cases/bdd/features/search.feature"]),
        "bdd-cart": lambda: run(["behave", "test_cases/bdd/features/cart.feature"]),
        "bdd-e2e": lambda: run(["behave", "test_cases/bdd/features/e2e.feature"]),
    }

    # 特殊命令
    def run_report():
        pytest_cmd("--alluredir=reports/allure-results", "--clean-alluredir")
        return run(["allure", "serve", "reports/allure-results"])

    def run_bdd_report():
        behave_cmd(
            "-f", "allure_behave.formatter:AllureFormatter",
            "-o", "reports/allure-results",
        )
        return run(["allure", "serve", "reports/allure-results"])

    def run_all():
        pytest_cmd()
        return behave_cmd()

    def run_headless():
        return pytest_cmd("--headed=false")

    def run_setup():
        from allure_config import write_categories, write_environment, write_executor
        write_categories()
        write_environment()
        write_executor()

    special = {
        "all": run_all,
        "report": run_report,
        "bdd-report": run_bdd_report,
        "headless": run_headless,
        "setup": run_setup,
    }

    all_commands = {**pom_modules, **kdt_commands, **bdd_commands, **special}

    if cmd in all_commands:
        exit_code = all_commands[cmd]()
    else:
        print(f"未知命令:{cmd}")
        print(f"支持的命令:{', '.join(sorted(all_commands.keys()))}")
        exit_code = 1

    sys.exit(exit_code)


if __name__ == "__main__":
    main()

六、项目完整结构总览

复制代码
web_ui/
├── config/                             ← 配置层
│   ├── __init__.py
│   ├── config.py                       ← 多环境配置管理
│   └── env_config.yaml                 ← dev / staging / prod
│
├── common/                             ← 工具层
│   ├── __init__.py
│   ├── logger.py                       ← 日志封装
│   ├── data_reader.py                  ← JSON/YAML/CSV/Excel 读取
│   ├── screenshot.py                   ← 截图工具
│   ├── random_data.py                  ← 随机数据生成
│   └── allure_helper.py               ← Allure 报告辅助
│
├── pages/                              ← POM 页面对象层(三种模式共用)
│   ├── __init__.py
│   ├── base_page.py                    ← 基类(40+ 通用方法)
│   ├── login_page.py                   ← 登录页
│   ├── home_page.py                    ← 首页
│   ├── product_page.py                 ← 商品详情页
│   ├── cart_page.py                    ← 购物车页
│   ├── order_page.py                   ← 订单页
│   └── profile_page.py                 ← 个人中心页
│
├── keywords/                           ← KDT 关键字层
│   ├── __init__.py
│   ├── base_keywords.py                ← 底层通用关键字(25 个)
│   ├── login_keywords.py               ← 登录业务关键字(7 个)
│   ├── search_keywords.py              ← 搜索业务关键字(8 个)
│   ├── cart_keywords.py                ← 购物车关键字(9 个)
│   ├── order_keywords.py               ← 订单关键字(4 个)
│   └── keyword_registry.py             ← 关键字注册表(53 个)
│
├── engine/                             ← KDT 驱动引擎
│   ├── __init__.py
│   └── test_engine.py                  ← 支持 KDT + DDT + 变量替换
│
├── test_cases/
│   ├── __init__.py
│   ├── pom/                            ← POM 模式用例
│   │   ├── __init__.py
│   │   ├── test_login.py               ← 登录(10 条)
│   │   ├── test_search.py              ← 搜索(10 条)
│   │   ├── test_product.py             ← 商品(4 条)
│   │   ├── test_cart.py                ← 购物车(5 条)
│   │   ├── test_order.py               ← 订单(3 条)
│   │   └── test_e2e.py                 ← 端到端(4 条)
│   ├── kdt/                            ← KDT + DDT 模式用例
│   │   ├── __init__.py
│   │   ├── test_kdt_runner.py          ← KDT 运行器
│   │   ├── login/
│   │   │   ├── test_login.yaml         ← 纯 KDT(8 条)
│   │   │   └── test_login_ddt.yaml     ← KDT+DDT(11 条)
│   │   ├── search/
│   │   │   ├── test_search.yaml        ← 纯 KDT(10 条)
│   │   │   └── test_search_ddt.yaml    ← KDT+DDT(9 条)
│   │   └── cart/
│   │       └── test_cart.yaml          ← 纯 KDT(5 条)
│   └── bdd/                            ← BDD 模式用例
│       ├── __init__.py
│       ├── environment.py              ← 浏览器生命周期
│       ├── features/
│       │   ├── login.feature           ← 登录(14 条)
│       │   ├── search.feature          ← 搜索(10 条)
│       │   ├── cart.feature            ← 购物车(4 条)
│       │   └── e2e.feature             ← 端到端(2 条)
│       └── steps/
│           ├── login_steps.py
│           ├── search_steps.py
│           ├── cart_steps.py
│           └── common_steps.py
│
├── test_data/                          ← 测试数据
│   ├── login_data.json
│   ├── login_fail_data.yaml
│   ├── login_success_data.yaml
│   ├── search_data.yaml
│   ├── search_ddt_data.yaml
│   ├── category_ddt_data.yaml
│   └── cart_test_data.yaml
│
├── reports/                            ← 报告输出(运行时生成)
│   ├── screenshots/
│   ├── logs/
│   ├── allure-results/
│   └── junit.xml
│
├── .github/workflows/ui-test.yml      ← GitHub Actions CI/CD
├── Jenkinsfile                         ← Jenkins Pipeline
├── allure_config.py                    ← Allure 配置生成
├── conftest.py                         ← Pytest 核心 fixture
├── behave.ini                          ← BDD 配置
├── pytest.ini                          ← Pytest 配置
├── run.py                              ← 一键运行脚本
├── requirements.txt                    ← 依赖清单
└── venv/                               ← 虚拟环境

七、用例总览

模式 模块 用例数
POM 登录 10
搜索 10
商品 4
购物车 5
订单 3
端到端 4
小计 36
KDT 登录 8
搜索 10
购物车 5
小计 23
KDT+DDT 登录 11
搜索 9
小计 20
BDD 登录 14
搜索 10
购物车 4
端到端 2
小计 30
合计 109

八、关键技术点回顾

接着来,从 8.1 继续。


8.1 每篇文章的核心产出

核心产出 代码量
01 项目总览 项目骨架 + 12 个基础用例 ~200 行
02 基础层 config + common + test_data + conftest ~600 行
03 POM 实现 BasePage(40 方法) + 6 个页面对象 ~900 行
04 POM 用例 36 条完整测试用例 ~500 行
05 KDT 实现 53 个关键字 + 注册表 + 引擎 ~800 行
06 KDT+DDT 引擎升级 + DDT 数据源 + 20 条展开用例 ~300 行
07 BDD 4 个 Feature + 4 个 Steps ~500 行
08 企业落地 Allure 完整集成 + CI/CD + 策略文档 ~400 行
合计 109 条用例 + 完整框架 ~4200 行

8.2 框架各层的核心价值

复制代码
config/(配置层)
  → 价值:一处改 URL,全项目生效
  → 不用这个:改 109 条用例中的 URL

common/(工具层)
  → 价值:日志、截图、数据读取、报告统一管理
  → 不用这个:每个文件各写一套,风格不统一

pages/(页面对象层)
  → 价值:前端改了元素 id,只改 1 个文件
  → 不用这个:改 36 条 POM 用例 + 23 条 KDT 用例 + 30 条 BDD 用例

keywords/(关键字层)
  → 价值:非技术人员用 YAML 写用例
  → 不用这个:所有用例必须会 Python

engine/(驱动引擎)
  → 价值:一条模板跑 N 组数据
  → 不用这个:复制粘贴 N 份 YAML

test_data/(数据层)
  → 价值:新增测试数据不改代码
  → 不用这个:每次加数据都改 Python 文件

conftest.py(核心配置)
  → 价值:浏览器生命周期统一管理,失败自动截图
  → 不用这个:每个用例手动创建和销毁浏览器

8.3 关键技术选型回顾

决策点 选择 原因
浏览器驱动 Playwright 自动等待、原生同步/异步、强大定位器
测试框架 Pytest fixture 机制、插件丰富、零样板代码
设计模式 POM + KDT + BDD 组合使用覆盖不同场景
数据驱动 JSON + YAML + CSV + Excel 覆盖主流数据格式
报告 Allure 层级清晰、附件丰富、CI 友好
CI/CD GitHub Actions + Jenkins 覆盖主流 CI 平台

九、常见问题 FAQ

9.1 环境问题

Q:Playwright 安装后运行报错 "Browser not found"

bash 复制代码
# 重新安装浏览器驱动
playwright install chromium

# 如果网络问题,设置镜像
PLAYWRIGHT_DOWNLOAD_HOST=https://npmmirror.com/mirrors/playwright playwright install chromium

Q:MallLite 启动后测试访问不到

bash 复制代码
# 检查 MallLite 是否正常运行
curl http://localhost:8000/

# 检查端口是否被占用
netstat -ano | findstr 8000     # Windows
lsof -i :8000                   # macOS/Linux

# 如果用 conftest 中的 base_url 配置了其他地址,检查地址是否正确

Q:Allure 报告打开是空白页

bash 复制代码
# 确认 Allure 版本
allure --version

# 确认数据目录有内容
ls reports/allure-results/

# 如果目录为空,先运行测试生成数据
pytest -v --alluredir=reports/allure-results

# 重新生成报告
allure generate reports/allure-results -o reports/allure-report --clean
allure open reports/allure-report

9.2 用例问题

Q:元素定位失败 "waiting for selector ... timeout"

python 复制代码
# 原因 1:选择器写错了
# 打开浏览器开发者工具,确认选择器
page.locator("#correct-selector")

# 原因 2:页面还没加载完
# 加等待
page.wait_for_load_state("networkidle")
page.wait_for_selector("#element", state="visible")

# 原因 3:元素在 iframe 中
frame = page.frame_locator("iframe#main")
frame.locator("#element").click()

# 原因 4:元素在新窗口中
with page.expect_popup() as popup_info:
    page.click("#open-new-window")
new_page = popup_info.value
new_page.locator("#element").click()

Q:用例在本地通过,CI 环境失败

bash 复制代码
# 常见原因:
# 1. 无头模式差异 → 确保 CI 用 --headed=false
# 2. 分辨率差异 → 在 context 中固定 viewport
# 3. 网络差异 → CI 环境增加超时时间
# 4. 时序差异 → 用 wait_for 代替 sleep

# 排查方法:在 CI 中截图
- name: Debug Screenshot
  if: failure()
  uses: actions/upload-artifact@v4
  with:
    name: debug-screenshot
    path: web_ui/reports/screenshots/

Q:DDT 展开后用例名重复

yaml 复制代码
# 确保 data_source 中有唯一字段用于命名
- name: "登录失败 - ${case_name}"  # case_name 在数据中唯一
  data_source: "login_fail_data.yaml"
  steps: ...

9.3 框架扩展问题

Q:如何添加一个新的页面?

复制代码
三步操作:

第一步:创建页面对象
  pages/new_page.py → 继承 BasePage

第二步:添加 fixture
  conftest.py → @pytest.fixture def new_page(page): return NewPage(page)

第三步:编写用例
  test_cases/pom/test_new.py → 使用 new_page fixture

Q:如何添加一个新的关键字?

复制代码
两步操作:

第一步:在关键字类中添加方法
  keywords/xxx_keywords.py → def new_keyword(self, param): ...

  方法名就是关键字名,不需要额外注册
  KeywordRegistry 自动扫描所有公开方法

第二步:在 YAML 中使用
  - keyword: new_keyword
    params:
      param: "value"

Q:如何支持新的浏览器?

bash 复制代码
# 在 conftest.py 中已有浏览器选择逻辑
# 运行时指定即可
pytest --browser=firefox -v
pytest --browser=webkit -v

# 确保已安装对应驱动
playwright install firefox
playwright install webkit

Q:如何对接企业内部系统(如飞书、钉钉通知)?

python 复制代码
# 在 conftest.py 的 pytest_sessionfinish 中添加通知逻辑

def pytest_sessionfinish(session, exitstatus):
    """测试结束后发送通知"""
    passed = session.testscollected - session.testsfailed
    total = session.testscollected
    failed = session.testsfailed

    report_url = "https://your-allure-report-url.com"

    # 飞书通知示例
    if failed > 0:
        send_feishu_notification(
            title=f"UI 测试完成:{passed}/{total} 通过",
            content=f"失败 {failed} 条\n报告:{report_url}",
        )

十、Web UI 自动化篇总结

10.1 八篇文章的知识图谱

复制代码
01 项目总览与环境搭建
│   → Playwright 选型、fixture、marker、第一个测试
│
02 框架基础层搭建
│   → config 配置管理、common 工具层、test_data 数据层
│
03 POM 模式原理与实现
│   → BasePage 基类、6 个页面对象、调用链路
│
04 POM 实战用例
│   → 36 条完整用例、数据驱动、Allure 标记
│
05 KDT 模式原理与实现
│   → 关键字层、注册表、驱动引擎、YAML 用例
│
06 KDT + DDT 实战
│   → 变量替换、数据源加载、模板展开
│
07 BDD 模式实战
│   → Behave 框架、Feature 文件、Step Definitions
│
08 企业选型·报告·CI/CD
│   → Allure 完整集成、GitHub Actions、Jenkins、策略文档
│
└── 产出:109 条用例 + 完整企业级 UI 自动化框架

10.2 能力成长路线

通过这 8 篇文章,你从"会用 Playwright 打开网页"成长到了能独立搭建企业级自动化测试框架:

复制代码
之前:手动打开浏览器 → 输入 → 点击 → 眼睛看结果

现在:
  → 109 条用例自动执行
  → POM / KDT / BDD 三种模式按需选择
  → 外部数据文件驱动,新增场景不改代码
  → Allure 报告自动生成,失败自动截图
  → CI/CD 自动触发,定时执行
  → 一份完整的自动化测试策略文档

十一、系列导航

序号 标题 状态
Python 基础篇(P01-P06) 从零到能写 FastAPI 后端 ✅ 已完成
Web UI 自动化篇
01 项目总览与环境搭建
02 框架基础层搭建
03 POM 模式原理与实现
04 POM 实战用例
05 KDT 模式原理与实现
06 KDT + DDT 实战
07 BDD 模式实战
08 企业选型·报告·CI/CD ✅ 本文
接口自动化篇
09 接口自动化基础与环境 下一篇
10 接口自动化框架搭建 待更新
11 接口自动化实战 待更新
12 接口自动化进阶 待更新
13 接口数据驱动与报告 待更新

Web UI 自动化篇完结。下一篇进入接口自动化篇------用 Requests 库 + 自研框架对 MallLite 的全部 API 接口进行自动化测试。

相关推荐
维克兜率天20 分钟前
4.1.3 策略类型全景图:六大策略,你适合哪个
笔记·python·算法·量化
IT毕设实战小研34 分钟前
基于安卓的考研资讯系统设计与实现
android·大数据·vue.js·爬虫·python·考研·课程设计
卷无止境3 小时前
Python能做嵌入式开发吗?一份写给动手派的生态与硬件全景图
后端·python
月光船幽幽6 小时前
检测用户意图复杂性的关键方法
人工智能·python
威联通网络存储9 小时前
TS-h3087XU-RP 汽车零部件质检追溯部署
python·汽车
你驴我10 小时前
WhatsApp 多账号场景下的会话归档与历史消息检索优化实践
后端·python
IT毕设实战小研10 小时前
基于安卓的空气质量查询系统
android·大数据·vue.js·爬虫·python·django·课程设计
harmful_sheep10 小时前
java group by常见用法
java·开发语言·python
whcyhhh11 小时前
头歌实践教学平台:数据科学与大数据技术导论(十六)
大数据·开发语言·python