目录

一、为什么要做这套自动化测试平台?

二、先看全貌:项目架构与目录结构

三、核心技术栈一览

四、核心基石:PO 模式 + 通用操作封装,告别维护噩梦

1. 通用元素操作封装(operateElement.py)

2. 页面元素层(page 层)

3. 业务逻辑层(business 层)

4. 测试用例层(test_case 层)

五、核心亮点:Allure 报告深度定制 + 历史趋势保留

核心实现逻辑

完整的核心执行代码(testRunner.py)

补全可用的 Web 控制面板页面

六、环境搭建与快速启动,一步到位

1. 前置环境准备

2. 依赖包一键安装

3. 快速启动运行

七、进阶优化方向(可直接扩展)

八、结尾总结


告别手工回归测试的重复痛点,实现一键触发测试、定时无人值守执行、可视化精美报告、历史趋势全追踪,测试人可直接落地的全栈自动化方案,所有代码复制即可用。

一、为什么要做这套自动化测试平台?

做测试的同学一定都遇到过这些痛点:

  • 迭代快、回归场景多,手工点点点耗时耗力,还容易漏测
  • 自动化脚本只能本地 / 服务器命令行执行,非技术同学无法操作
  • 测试报告零散、不规范,无法追溯历史执行趋势,问题定位难
  • 定时回归、夜间巡检需求,无法实现无人值守自动执行

今天这套完整方案,一次性解决以上所有问题:

✅ 基于 PO 设计模式,代码耦合度极低,用例可维护性拉满

✅ 封装通用元素操作,新手也能快速编写业务用例

✅ Web 可视化控制面板,一键执行测试,无需敲任何命令

✅ 深度定制 Allure 报告,自带环境信息、历史趋势、失败自动截图

✅ 内置定时任务调度,轻松实现夜间自动回归、定时巡检

✅ 全量代码开箱即用,替换业务逻辑即可快速落地到自己的项目

二、先看全貌:项目架构与目录结构

先给大家看完整的项目目录结构,清晰的分层是自动化框架可维护的核心:

每个目录的作用一目了然,严格遵循 PO 设计模式的分层思想:

目录 / 文件核心作用
business 层业务逻辑封装,比如登录、通用业务流程,减少用例冗余代码
page 层页面元素统一管理,所有元素定位集中存放,一改全改
test_case 层测试用例编写,只聚焦业务场景,不关心底层元素和操作
reports 目录测试原始数据、Allure 报告、历史趋势数据统一存放
templates 目录Flask 前端页面模板,存放 Web 控制面板页面
testRunner.py框架核心启动文件,集成测试执行、Flask 服务、定时任务全能力

三、核心技术栈一览

本方案用到的技术栈都是测试圈主流、稳定、社区活跃的工具,无小众依赖,新手也能快速掌握:

技术栈核心作用
Python 3.8+开发语言,自动化测试的首选
Selenium主流 UI 自动化工具,支持 Chrome 等多浏览器
Pytest最流行的 Python 测试框架,灵活度高、插件生态丰富
Allure高颜值测试报告生成工具,支持步骤追溯、截图、趋势统计
Flask轻量级 Web 框架,快速搭建可视化控制面板
APScheduler定时任务框架,实现后台无人值守自动执行测试
PO 设计模式自动化框架核心设计思想,实现页面、元素、用例解耦

四、核心基石:PO 模式 + 通用操作封装,告别维护噩梦

很多人写自动化脚本,最后变成 “一次性脚本”,核心问题就是没有做好分层和封装。这套框架先从底层解决可维护性问题。

1. 通用元素操作封装(operateElement.py)

我们把 Selenium 的高频操作(点击、输入、断言、截图等)全部封装,统一处理显式等待、异常捕获、失败截图、Allure 步骤埋点,不用在每个用例里重复写冗余代码。

完整可直接使用的代码:

import time
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.support.ui import WebDriverWait
import allure

class OperateElement:
    def __init__(self, driver):
        self.driver = driver

    def click(self, locator):
        """封装点击操作,自带等待、异常重试、失败截图"""
        with allure.step('点击元素'):
            try:
                WebDriverWait(self.driver, 10).until(ec.element_to_be_clickable((By.XPATH, locator))).click()
            except:
                allure.attach(self.driver.get_screenshot_as_png(),name='点击元素失败',attachment_type=allure.attachment_type.PNG)
                # 失败重试一次
                WebDriverWait(self.driver, 10).until(ec.element_to_be_clickable((By.XPATH, locator))).click()

    def send_keys(self, locator, text):
        """封装输入操作,自带等待、失败截图"""
        with allure.step(f'输入内容:{text}'):
            try:
                WebDriverWait(self.driver, 10).until(ec.element_to_be_clickable((By.XPATH, locator))).send_keys(text)
            except:
                allure.attach(self.driver.get_screenshot_as_png(),name='输入内容失败',attachment_type=allure.attachment_type.PNG)
                assert False, f"元素{locator}输入失败"

    def get_text(self, locator):
        """封装获取文本操作,自带等待、失败截图"""
        with allure.step('获取元素文本'):
            try:
                return WebDriverWait(self.driver, 10).until(ec.presence_of_element_located((By.XPATH, locator))).text
            except:
                allure.attach(self.driver.get_screenshot_as_png(),name='获取文本失败',attachment_type=allure.attachment_type.PNG)
                return WebDriverWait(self.driver, 10).until(ec.presence_of_element_located((By.XPATH, locator))).text

    def get_element(self, locator):
        """封装获取元素操作,自带等待、失败截图"""
        with allure.step('获取页面元素'):
            try:
                return WebDriverWait(self.driver, 10).until(ec.presence_of_element_located((By.XPATH, locator)))
            except:
                allure.attach(self.driver.get_screenshot_as_png(),name='获取元素失败',attachment_type=allure.attachment_type.PNG)
                return WebDriverWait(self.driver, 10).until(ec.presence_of_element_located((By.XPATH, locator)))

    def clear(self, locator):
        """封装清空输入框操作,自带等待、失败截图"""
        with allure.step('清除输入框文本'):
            try:
                WebDriverWait(self.driver, 10).until(ec.presence_of_element_located((By.XPATH, locator))).clear()
            except:
                allure.attach(self.driver.get_screenshot_as_png(),name='清除文本失败',attachment_type=allure.attachment_type.PNG)
                WebDriverWait(self.driver, 10).until(ec.presence_of_element_located((By.XPATH, locator))).clear()

    def assert_text(self, xpath, expect_text):
        """封装文本断言,自带等待、截图、详细报错信息"""
        time.sleep(1)
        with allure.step(f'验证元素包含文本:{expect_text}'):
            try:
                actual_text = self.get_text(xpath)
                assert expect_text in actual_text, f"文本断言失败!预期包含:'{expect_text}',实际文本:'{actual_text}'"
            except Exception as e:
                allure.attach(self.driver.get_screenshot_as_png(),name='断言文本失败',attachment_type=allure.attachment_type.PNG)
                raise

2. 页面元素层(page 层)

把每个页面的元素定位统一管理,彻底告别在代码里到处写 XPATH 的问题。后续页面元素变更,只需要改这一个文件,不用修改上百条用例。

以项目管理页面为例,完整代码(projectManagementPage.py):

class ProjectManagementPage:
    # 导航栏元素
    projectManagementLi='//*[@id="sidebar-container-wrap"]/div[1]/div[1]/div/ul/li[2]/span'
    # 项目相关元素
    addProject='//*[@id="layout-classic-wrap"]/div[2]/div[2]/section/div/div[1]/div[1]/div[2]/button[2]'
    projectName='/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/section/div/div[3]/div/div/div/div/div/div[2]/form/div[1]/div[2]/div/div/input'
    projectAddress='/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/section/div/div[3]/div/div/div/div/div/div[2]/form/div[2]/div[2]/div/div/div/input'
    projectAddressUl='/html/body/div[3]/div[6]/div[2]/div[1]/ul'
    projectAddressUlLi='/html/body/div[3]/div[6]/div[2]/div[1]/ul/li'
    projectIntroduction='/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/section/div/div[3]/div/div/div/div/div/div[2]/form/div[3]/div[2]/div/textarea'
    confirm='/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/section/div/div[3]/div/div/div/footer/div/div/button[1]'
    more='/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/section/div/div[2]/div[1]/div[3]/div[2]/div[3]/div'
    deleteProject='/html/body/div[3]/div[6]/div/div[2]'
    deleteConfirm='/html/body/div[4]/div/div/div[3]/button[2]'
    # 分组相关元素
    addGroup='/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/section/div/div[1]/div[1]/div[2]/button[1]'
    groupName='/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/section/div/div[5]/div/div/div/div/form/div/div[2]/div/div/input'
    groupAddConfirm='/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/section/div/div[5]/div/div/div/footer/div/div/button[1]'
    groupMore='/html/body/div[1]/div[1]/div[1]/div/div[2]/div[2]/section/div/div[2]/div[2]/div/div/button/div/div[2]/div'
    groupUpdate='/html/body/div[3]/div[6]/div/div[2]'
    groupDelete='/html/body/div[3]/div[6]/div/div[1]'

3. 业务逻辑层(business 层)

把高频的业务流程(比如登录、进入模块)封装成通用方法,用例里一行代码就能完成复杂的业务前置操作,大幅减少代码冗余。

这里补充登录业务的完整示例(loginBusiness.py),可直接使用:

from page.login_page import LoginPage
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait

class LoginBusiness:
    @classmethod
    def loginBusiness(self, driver, name, password):
        WebDriverWait(driver,10).until(EC.visibility_of_element_located((By.XPATH,LoginPage.login))).click()
        driver.find_element(*LoginPage.username).send_keys(name)
        driver.find_element(*LoginPage.password).send_keys(password)
        driver.find_element(*LoginPage.button).click()

同时把元素操作类也放到 business 层,目录结构完全匹配。

4. 测试用例层(test_case 层)

有了前面的三层封装,用例编写变得极其简洁,只需要聚焦业务场景,不用关心底层实现。同时集成 Allure 装饰器,生成的报告结构清晰、可追溯。

完整的项目管理模块测试用例(test_projectManagement.py):

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait
from business.loginBusiness import LoginBusiness
from page.projectManagementPage import ProjectManagementPage as pmp
import time,allure
from selenium.webdriver.chrome.options import Options
from business.operateElement import OperateElement

@allure.feature('项目管理模块')
class TestProjectMangement:
    def setup_method(self):
        """用例前置操作:初始化浏览器、登录、进入对应模块"""
        # 配置无头浏览器,服务器运行也不会报错
        chrome_options = Options()
        chrome_options.add_argument("--headless")
        chrome_options.add_argument("--no-sandbox")
        chrome_options.add_argument("--disable-dev-shm-usage")
        chrome_options.add_argument("--disable-gpu")
        # 初始化浏览器
        self.driver = webdriver.Chrome(options=chrome_options)
        self.driver.maximize_window()
        self.driver.get('https://single.swarm-bim.com/login?appId=85319726')
        time.sleep(3)
        # 调用封装好的登录业务
        LoginBusiness.loginBusiness(self.driver, '173******62', '70**62')
        time.sleep(2)
        # 进入项目管理模块
        OperateElement(self.driver).click(pmp.projectManagementLi)
        time.sleep(6)

    @allure.title('正常创建项目场景')
    def test_prjecctMangemtAdd(self):
        OperateElement(self.driver).click(pmp.addProject)
        OperateElement(self.driver).send_keys(pmp.projectName, '自动化测试添加项目')
        OperateElement(self.driver).send_keys(pmp.projectAddress, '北京-北京')
        time.sleep(1)
        # 处理地址下拉框
        option_list = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located(
            (By.XPATH, pmp.projectAddressUl)))
        time.sleep(1)
        target_option = option_list.find_element(By.XPATH, pmp.projectAddressUlLi)
        target_option.click()
        OperateElement(self.driver).send_keys(pmp.projectIntroduction, '自动化测试简介')
        OperateElement(self.driver).click(pmp.confirm)
        # 断言结果
        OperateElement(self.driver).assert_text( '//div[@role="alert"]','项目创建成功')
        # 操作完成截图,存入报告
        with allure.step('创建成功截图'):
            allure.attach(self.driver.get_screenshot_as_png(),name='正常创建项目',attachment_type=allure.attachment_type.PNG)

    @allure.title('删除项目场景')
    def test_deleteProject(self):
        time.sleep(3)
        OperateElement(self.driver).click(pmp.more)
        OperateElement(self.driver).click(pmp.deleteProject)
        OperateElement(self.driver).click(pmp.deleteConfirm)
        OperateElement(self.driver).assert_text( '//div[@role="alert"]','删除项目成功')
        with allure.step('删除成功截图'):
            allure.attach(self.driver.get_screenshot_as_png(),name='删除项目',attachment_type=allure.attachment_type.PNG)

    @allure.title('正常创建分组场景')
    def test_addGroup01(self):
        time.sleep(3)
        OperateElement(self.driver).click(pmp.addGroup)
        OperateElement(self.driver).send_keys(pmp.groupName, '自动化测试添加分组')
        OperateElement(self.driver).click(pmp.groupAddConfirm)
        OperateElement(self.driver).assert_text( '//div[@role="alert"]','分组创建成功')
        with allure.step('创建分组成功截图'):
            allure.attach(self.driver.get_screenshot_as_png(),name='正常创建分组',attachment_type=allure.attachment_type.PNG)

    @allure.title('添加已存在的分组场景-反向用例')
    def test_addGroup02(self):
        time.sleep(3)
        OperateElement(self.driver).click(pmp.addGroup)
        OperateElement(self.driver).send_keys(pmp.groupName, '自动化测试添加分组')
        OperateElement(self.driver).click(pmp.groupAddConfirm)
        OperateElement(self.driver).assert_text( '//div[@role="alert"]','已存在分组名称,请勿重复添加')
        with allure.step('反向用例截图'):
            allure.attach(self.driver.get_screenshot_as_png(),name='添加已存在的分组',attachment_type=allure.attachment_type.PNG)

    @allure.title('重命名分组场景')
    def test_updateGroup(self):
        time.sleep(1)
        OperateElement(self.driver).click(pmp.groupMore)
        OperateElement(self.driver).click(pmp.groupUpdate)
        OperateElement(self.driver).clear(pmp.groupName)
        OperateElement(self.driver).send_keys(pmp.groupName, '自动化测试重命名分组')
        OperateElement(self.driver).click(pmp.groupAddConfirm)
        OperateElement(self.driver).assert_text( '//div[@role="alert"]','重命名分组成功')
        with allure.step('重命名成功截图'):
            allure.attach(self.driver.get_screenshot_as_png(),name='重命名分组',attachment_type=allure.attachment_type.PNG)

    @allure.title('删除分组场景')
    def test_deleteGroup(self):
        time.sleep(1)
        OperateElement(self.driver).click(pmp.groupMore)
        OperateElement(self.driver).click(pmp.groupDelete)
        OperateElement(self.driver).click(pmp.deleteConfirm)
        OperateElement(self.driver).assert_text( '//div[@role="alert"]','删除分组成功')
        with allure.step('删除分组成功截图'):
            allure.attach(self.driver.get_screenshot_as_png(),name='删除分组',attachment_type=allure.attachment_type.PNG)

    def teardown_method(self):
        """用例后置操作:关闭浏览器,释放资源"""
        self.driver.quit()

五、核心亮点:Allure 报告深度定制 + 历史趋势保留

很多同学用 Allure,只实现了基础的报告生成,却没有环境信息、没有历史执行趋势,报告的实用性大打折扣。这套框架里,我们做了全量的定制化优化。

核心实现逻辑

完整的测试执行 + 报告生成核心代码,都在 testRunner.py 中,我们拆解核心知识点:

  1. 环境信息自动生成自动写入项目信息、Python 版本、浏览器版本、操作系统、执行时间等,报告里直接展示,不用手动填写。
  2. 历史趋势完整保留这是 Allure 报告的核心痛点:默认生成的报告没有历史趋势。解决方法很简单:每次执行前,把上一次报告的 history 文件夹复制到本次的原始数据目录;生成新报告后,再把新的 history 保存下来,实现趋势持续追踪。
  3. 报告个性化定制修改报告浏览器标题、报告名称,完全贴合自己的项目,告别默认的 Allure 标题。

完整的核心执行代码(testRunner.py)

包含测试执行、报告生成、Flask Web 服务、定时任务全能力,复制即可用:

import json
import pytest
import os
import datetime
import shutil
from pathlib import Path
import platform
from flask import Flask, render_template, send_from_directory, url_for, jsonify
from apscheduler.schedulers.background import BackgroundScheduler

# 初始化Flask应用
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'

# 全局路径配置(统一管理,修改一处全量生效)
REPORTS_DIR = Path("reports")
HISTORY_DIR = REPORTS_DIR / "allure-history/history"
HTML_REPORT_DIR = REPORTS_DIR / "html"

def generate_timestamp():
    """生成时间戳,用于区分每次执行的原始数据"""
    return datetime.datetime.now().strftime("%Y%m%d_%H%M%S")

def create_environment_file(raw_dir):
    """创建Allure环境信息文件,报告中展示执行环境详情"""
    env_file = raw_dir / "environment.properties"
    env_content = f"""projectName=SwarmBIM
pythonVersion={platform.python_version()}
allureVersion=2.13.8
baseUrl=https://single.swarm-bim.com
executionTime={datetime.datetime.now().isoformat()}
author=TestTeam
osName={platform.system()}
osVersion={platform.release()}
browserName=Chrome
browserVersion=87.0.4280.88
"""
    with env_file.open('w', encoding='utf-8-sig') as f:
        f.write(env_content)

def run_tests():
    """核心方法:执行测试、生成报告、处理历史数据"""
    timestamp = generate_timestamp()
    raw_dir = REPORTS_DIR / f"raw_{timestamp}"
    
    # 1. 创建目录结构
    raw_dir.mkdir(parents=True, exist_ok=True)
    HISTORY_DIR.mkdir(parents=True, exist_ok=True)
    
    # 2. 复制历史数据,用于生成趋势图(核心!解决无历史趋势的问题)
    if HISTORY_DIR.exists():
        shutil.copytree(
            HISTORY_DIR,
            raw_dir / "history",
            dirs_exist_ok=True
        )
    
    # 3. 执行pytest测试用例,生成Allure原始数据
    pytest_args = [
        'test_case', # 测试用例目录,可自行修改
        f'--alluredir={raw_dir}',
        '--clean-alluredir'
    ]
    exit_code = pytest.main(pytest_args)
    
    # 4. 生成环境信息文件
    create_environment_file(raw_dir)
    
    # 5. 生成Allure HTML报告,--clean覆盖旧报告
    os.system(f'allure generate {raw_dir} -o {HTML_REPORT_DIR} --clean')
    
    # 6. 保存本次报告的历史数据,供下次执行使用
    if (HTML_REPORT_DIR / 'history').exists():
        shutil.copytree(
            HTML_REPORT_DIR / 'history',
            HISTORY_DIR,
            dirs_exist_ok=True
        )
    
    # 7. 自定义报告标题
    index_html = HTML_REPORT_DIR / "index.html"
    if index_html.exists():
        with open(index_html, "r+", encoding="utf-8") as f:
            content = f.read().replace(
                '<title>Allure Report</title>',
                '<title>SIM协同系统测试报告</title>'
            )
            f.seek(0)
            f.write(content)
            f.truncate()
    
    # 8. 自定义报告名称
    summary_path = HTML_REPORT_DIR / "widgets/summary.json"
    if summary_path.exists():
        data = json.loads(summary_path.read_text())
        data['reportName'] = 'SIM协同系统正式服测试报告'
        summary_path.write_text(json.dumps(data))
    
    return HTML_REPORT_DIR / "index.html"

# ---------------------- Flask Web路由配置 ----------------------
@app.route('/')
def control_panel():
    """首页:测试控制面板"""
    return render_template('report_control.html')

@app.route('/run-tests', methods=['POST'])
def trigger_tests():
    """接口:一键触发测试执行"""
    try:
        report_path = run_tests()
        return jsonify({
            'status': 'success',
            'report_url': url_for('serve_report', path=report_path.name)
        })
    except Exception as e:
        return jsonify({'status': 'error', 'message': str(e)}), 500

@app.route('/reports')
def list_reports():
    """接口:获取历史报告列表"""
    reports = []
    for f in sorted(HTML_REPORT_DIR.glob("*.html"), key=lambda x: x.stat().st_mtime, reverse=True):
        reports.append({
            'name': f"报告 - {datetime.datetime.fromtimestamp(f.stat().st_mtime).strftime('%Y-%m-%d %H:%M')}",
            'path': url_for('serve_report', path=f.name)
        })
    return jsonify(reports)

@app.route('/report/<path:path>')
def serve_report(path):
    """接口:在线查看Allure报告"""
    return send_from_directory(HTML_REPORT_DIR, path)

# ---------------------- 定时任务配置 ----------------------
def scheduled_test_job():
    """定时执行的测试任务"""
    with app.app_context():
        print(f"[{datetime.datetime.now()}] 开始定时测试任务...")
        run_tests()
        print("定时任务执行完成")

# 初始化后台调度器(守护进程,不阻塞Flask服务)
scheduler = BackgroundScheduler(daemon=True)
# 配置定时规则:cron表达式,这里默认每天00:21执行,可自行修改
scheduler.add_job(
    func=scheduled_test_job,
    trigger='cron',
    hour=0,
    minute=21,
    timezone='Asia/Shanghai'
)
# 启动定时任务
scheduler.start()

# ---------------------- 程序入口 ----------------------
if __name__ == '__main__':
    # 初始化必要目录,避免首次运行报错
    REPORTS_DIR.mkdir(exist_ok=True)
    HTML_REPORT_DIR.mkdir(exist_ok=True)
    # 启动Flask服务,host=0.0.0.0支持局域网访问,端口可自行修改
    app.run(host='0.0.0.0', port=5400, debug=False)

补全可用的 Web 控制面板页面

之前的 report_control.html 有报错,这里给大家提供一个完整可用、美观的前端页面,放到 templates 目录下即可:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>UI自动化测试平台</title>
    <script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: "Microsoft YaHei", sans-serif;
        }
        body {
            background-color: #f5f7fa;
            padding: 40px;
        }
        .container {
            max-width: 1200px;
            margin: 0 auto;
        }
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            border-radius: 12px;
            margin-bottom: 30px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.1);
        }
        .header h1 {
            font-size: 28px;
            margin-bottom: 10px;
        }
        .header p {
            opacity: 0.9;
            font-size: 16px;
        }
        .card {
            background: white;
            border-radius: 12px;
            padding: 30px;
            margin-bottom: 20px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.08);
        }
        .card h2 {
            font-size: 20px;
            margin-bottom: 20px;
            color: #303133;
        }
        .btn {
            padding: 12px 30px;
            border: none;
            border-radius: 6px;
            font-size: 16px;
            cursor: pointer;
            transition: all 0.3s;
        }
        .btn-primary {
            background-color: #667eea;
            color: white;
        }
        .btn-primary:hover {
            background-color: #5568d3;
        }
        .btn-primary:disabled {
            background-color: #c0c4cc;
            cursor: not-allowed;
        }
        .btn-link {
            background: none;
            color: #409eff;
            padding: 8px 0;
            text-decoration: underline;
        }
        .btn-link:hover {
            color: #66b1ff;
        }
        .loading {
            display: none;
            margin-left: 15px;
            color: #909399;
        }
        .report-list {
            list-style: none;
        }
        .report-item {
            padding: 15px 0;
            border-bottom: 1px solid #ebeef5;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .report-item:last-child {
            border-bottom: none;
        }
        .report-name {
            color: #303133;
            font-size: 16px;
        }
        .tips {
            margin-top: 15px;
            padding: 10px 15px;
            background-color: #ecf5ff;
            border-left: 4px solid #409eff;
            color: #409eff;
            border-radius: 4px;
        }
        .error-tips {
            margin-top: 15px;
            padding: 10px 15px;
            background-color: #fef0f0;
            border-left: 4px solid #f56c6c;
            color: #f56c6c;
            border-radius: 4px;
            display: none;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>UI自动化测试管理平台</h1>
            <p>基于Pytest+Selenium+Allure构建,一键执行测试 | 定时自动回归 | 可视化报告</p>
        </div>

        <div class="card">
            <h2>测试执行</h2>
            <button id="runBtn" class="btn btn-primary" onclick="runTests()">立即执行自动化测试</button>
            <span class="loading" id="loading">测试执行中,请耐心等待,请勿重复点击...</span>
            <div class="tips">执行完成后会自动跳转到测试报告页面,全程无需人工干预</div>
            <div class="error-tips" id="errorTips"></div>
        </div>

        <div class="card">
            <h2>历史报告</h2>
            <ul class="report-list" id="reportList">
                <li style="color: #909399;">加载中...</li>
            </ul>
        </div>
    </div>

    <script>
        // 页面加载时获取报告列表
        window.onload = function() {
            loadReportList();
        }

        // 执行测试
        function runTests() {
            const runBtn = document.getElementById('runBtn');
            const loading = document.getElementById('loading');
            const errorTips = document.getElementById('errorTips');
            
            // 禁用按钮,显示加载状态
            runBtn.disabled = true;
            loading.style.display = 'inline-block';
            errorTips.style.display = 'none';

            axios.post('/run-tests')
                .then(function (response) {
                    if (response.data.status === 'success') {
                        // 跳转到报告页面
                        window.location.href = response.data.report_url;
                    } else {
                        errorTips.innerText = '执行失败:' + response.data.message;
                        errorTips.style.display = 'block';
                    }
                })
                .catch(function (error) {
                    errorTips.innerText = '请求失败:' + error.message;
                    errorTips.style.display = 'block';
                })
                .finally(function () {
                    // 恢复按钮状态
                    runBtn.disabled = false;
                    loading.style.display = 'none';
                    // 重新加载报告列表
                    loadReportList();
                });
        }

        // 加载报告列表
        function loadReportList() {
            const reportList = document.getElementById('reportList');
            axios.get('/reports')
                .then(function (response) {
                    const reports = response.data;
                    if (reports.length === 0) {
                        reportList.innerHTML = '<li style="color: #909399;">暂无历史报告,先执行一次测试吧</li>';
                        return;
                    }
                    let html = '';
                    reports.forEach(item => {
                        html += `
                            <li class="report-item">
                                <span class="report-name">${item.name}</span>
                                <button class="btn btn-link" onclick="window.open('${item.path}')">查看报告</button>
                            </li>
                        `;
                    });
                    reportList.innerHTML = html;
                })
                .catch(function (error) {
                    reportList.innerHTML = '<li style="color: #f56c6c;">报告列表加载失败</li>';
                });
        }
    </script>
</body>
</html>

六、环境搭建与快速启动,一步到位

1. 前置环境准备

  1. Python 环境:安装 Python 3.8 及以上版本,配置好环境变量
  2. Chrome 与 ChromeDriver:安装 Chrome 浏览器,下载对应版本的 ChromeDriver, 将下载的ChromeDriver放在python安装的目录中(访问华为云镜像:Index of chromedriver-local)。
  3. Allure 命令行工具:

2. 依赖包一键安装

新建 requirements.txt 文件,复制以下内容,执行pip install -r requirements.txt即可安装所有依赖:

pytest>=7.0.0
selenium>=4.0.0
allure-pytest>=2.13.0
Flask>=2.0.0
APScheduler>=3.10.0

3. 快速启动运行

  1. 按照上面的目录结构,把所有代码放到对应文件夹里
  2. 修改自己的业务地址、账号密码、元素定位、测试用例
  3. 执行启动命令:python testRunner.py
  4. 打开浏览器,访问 http://127.0.0.1:5400 ,即可进入自动化测试控制面板
  5. 点击「立即执行自动化测试」,等待用例执行完成,自动跳转到 Allure 报告页面,和可查看以前执行的情况。

七、进阶优化方向(可直接扩展)

这套框架已经满足了企业级 UI 自动化的核心需求,还可以根据自己的团队场景,做进一步的扩展:

  1. 测试结果通知:增加钉钉 / 企业微信 / 邮件推送,测试执行完成自动推送结果和报告链接
  2. 多环境支持:配置测试环境、预发环境、正式环境,一键切换执行环境
  3. 并发执行提速:集成 pytest-xdist,实现多用例并发执行,大幅缩短执行时间
  4. 失败重试机制:增加用例失败自动重试,减少网络波动等偶发因素导致的误报
  5. 数据驱动测试:结合 @pytest.mark.parametrize,实现一套用例覆盖多组测试数据
  6. 容器化部署:把项目打包成 Docker 镜像,一键部署,无需在服务器配置环境
  7. 用户权限控制:增加登录鉴权功能,控制测试执行和报告查看的权限
  8. 用例管理功能:扩展用例筛选、指定用例执行、批量执行等能力

八、结尾总结

这套自动化测试平台,从底层的 PO 分层设计,到用例编写、执行管理、报告生成、定时任务,实现了 UI 自动化全流程的闭环。代码结构清晰,注释完整,新手也能快速上手,测试团队可以直接基于这套框架,快速落地自己的业务自动化用例。

不用再零散地找教程拼凑功能,这套方案直接给你完整可运行的生产级代码,复制就能用,改改业务逻辑就能落地到自己的项目里,大幅提升测试效率,告别重复的手工回归。

如果在使用过程中有任何问题,欢迎在评论区交流讨论~点赞收藏不迷路,后续会分享更多测试开发的落地干货!

更多推荐