一套可以直接落地的自动化测试框架,涵盖页面对象、业务封装、异常截图、Allure 报告、历史数据聚合等企业级特性,助你快速搭建稳定、可维护的 UI 自动化体系。


一、为什么要自己搭框架?

很多测试人员使用 Selenium 写脚本时,往往是“一个脚本一个样”,代码复用率低、维护成本高,更别提产出漂亮的报告了。
一套企业级自动化测试框架应当具备以下特征:

  • 分层设计:页面对象、业务逻辑、测试用例解耦
  • 封装 Selenium 原生操作:自动等待、异常截图、日志记录
  • 清晰的定位管理:统一维护元素定位表达式
  • 可读性强的测试用例:通过 pytest + allure 生成详实报告
  • 一键执行与历史数据聚合:方便持续集成(CI)

本文带你从 0 开始,基于 Python + Selenium + Pytest + Allure 构建一套可直接使用的 UI 自动化测试框架,所有代码均已提供,复制即可运行。


二、环境搭建(保姆级)

1. 安装 Python(3.8 及以上)

  • 官网下载:https://www.python.org/downloads/
  • 安装时勾选 Add Python to PATH
  • 验证:python --version

2. 安装 Python 依赖库

创建 requirements.txt,内容如下:

selenium==4.15.2
pytest==7.4.3
allure-pytest==2.13.2

执行安装:

pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

3. 安装 Chrome 浏览器及 ChromeDriver

  • Chrome 浏览器(建议最新版)
  • ChromeDriver 下载地址:https://chromedriver.chromium.org/
    确保驱动版本与浏览器版本匹配,并将 chromedriver.exe 所在目录加入系统 PATH 环境变量。
    或者使用 webdriver-manager 自动管理(可选)。

4. 安装 Allure 命令行工具

  • 下载 Allure 2.11.0 以上版本:https://github.com/allure-framework/allure2/releases
  • 解压后将 bin 目录加入系统 PATH
  • 验证:allure --version

三、项目目录结构(直接复制使用)

SwarmUIAutoTest/
├── business/               # 业务层(封装业务流程)
│   ├── __init__.py
│   ├── loginBusiness.py
│   └── operateElement.py   # Selenium 操作二次封装
├── page/                   # 页面对象层(元素定位)
│   ├── __init__.py
│   ├── login_page.py
│   └── projectManagementPage.py
├── test_case/              # 测试用例层
│   ├── __init__.py
│   └── test_projectManagement.py
├── reports/                # 测试报告输出目录(自动生成)
├── testRunner.py           # 执行入口 + 报告生成
└── requirements.txt

注意:每个目录下都需要创建一个空的 __init__.py 文件,使 Python 将其识别为模块包。


四、核心代码详解(可直接复制)

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('输入内容'):
            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

    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, text):
        time.sleep(1)
        with allure.step(f'验证元素[{xpath}]包含文本[{text}]'):
            try:
                element_text = self.get_text(xpath)
                assert text in element_text, f"文本断言失败!预期包含:'{text}',实际文本:'{element_text}'"
            except Exception as e:
                allure.attach(self.driver.get_screenshot_as_png(),
                              name='断言文本失败',
                              attachment_type=allure.attachment_type.PNG)
                raise

2. 页面对象层 login_page.py

统一管理登录页面的元素定位表达式,使用 By.XPATH 方式。

from selenium.webdriver.common.by import By

class LoginPage:
    login = '/html/body/div/div/div[2]/div/div/div/div[1]/div/div/div[2]/div'
    username = (By.XPATH, '//*[@id="normal_login1_userName"]')
    password = (By.XPATH, '/html/body/div/div/div[2]/div/div/div/div[2]/div/div[2]/form/div[2]/div/div/div/div/span/input')
    button = (By.XPATH, '/html/body/div/div/div[2]/div/div/div/div[2]/div/div[2]/form/div[3]/div/div/div/div/button')
    # 以下仅为示例,未实际使用
    asr01 = (By.XPATH, '//*[@id="navbar"]/div/div[3]/div/div[2]')
    asr02 = (By.XPATH, '/div/div/div/span[2]1')
    asr03 = (By.XPATH, '/html/body/div[1]/div/div[2]/div/div/div/div[2]/div/div[2]/form/div[2]/div[1]/div/div[2]/div[1]/div')

3. 业务层 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()

4. 页面对象层 projectManagementPage.py

项目管理模块中的所有元素定位,建议优先使用稳定的相对路径(如XPath、CSS选择器)。若相对路径无法成功定位,可考虑使用绝对定位作为备选方案。

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'  # 项目地点

5. 测试用例层 test_projectManagement.py

基于 pytest 编写测试用例,使用 setup_method / teardown_method 管理测试生命周期,调用封装的 OperateElement 进行操作。

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")  # 无头模式(可选)
        self.driver = webdriver.Chrome()
        self.driver.set_window_size(1920, 1080)
        self.driver.get('https://single.swarm-bim.com/login?appId=85319726')
        time.sleep(3)
        LoginBusiness.loginBusiness(self.driver, '173***562', '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_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()

6. 测试运行器 testRunner.py

集成了 pytest 执行、Allure 报告生成、历史数据聚合、环境变量配置,实现一键运行。

import json
import time
import pytest
import os
import datetime
import shutil
from pathlib import Path
import platform

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=3.8.5
allureVersion=2.11.0
baseUrl=https://single.swarm-bim.com/login?appId=85319726 
executionTime={time.strftime("%Y-%m-%d %H:%M:%S")}
author=HuangNanNing
osName={platform.system()}
osVersion={platform.release()}
browserName=Chrome
browserVersion=87.0.4280.88
browserSize=1920x1080
"""
    with env_file.open('w', encoding='utf-8-sig') as f:
        f.write(env_content)

def run_tests():
    timestamp = generate_timestamp()
    history_dir = Path("reports/allure-history/history")   # 独立存储历史数据
    raw_dir = Path(f"reports/raw_{timestamp}")             # 本次测试原始数据
    html_dir = Path("reports/html")                        # 最终报告目录

    raw_dir.mkdir(parents=True, exist_ok=True)
    history_dir.mkdir(parents=True, exist_ok=True)

    # 将历史数据复制到本次原始数据目录(Allure 要求 history 目录)
    if history_dir.exists():
        shutil.copytree(history_dir, raw_dir / "history", dirs_exist_ok=True)

    # 执行测试
    pytest_args = [
        'test_case',
        f'--alluredir={raw_dir}',
        '--clean-alluredir'
    ]
    exit_code = pytest.main(pytest_args)

    create_environment_file(raw_dir)

    # 生成 Allure 报告
    os.system(f'allure generate {raw_dir} -o {html_dir} --clean')

    # 保存本次报告中的 history 到独立目录,供下次使用
    if (html_dir / 'history').exists():
        shutil.copytree(html_dir / 'history', history_dir, dirs_exist_ok=True)

    # 修改报告标题
    with open(f"{html_dir}/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()

    # 修改 summary.json 中的报告名称
    summary_path = html_dir / "widgets/summary.json"
    data = json.loads(summary_path.read_text())
    data['reportName'] = 'SIM协同系统 正式服(项目策划、项目文档)测试报告'
    summary_path.write_text(json.dumps(data))

    os.system(f"allure open {html_dir}")
    return html_dir / "index.html"

if __name__ == "__main__":
    report_path = run_tests()
    print(f"报告生成路径:{report_path}")

五、如何运行测试?

  1. 确保 Chrome 浏览器已安装,且 ChromeDriver 已配置在 PATH 中。
  2. 将上述所有文件按目录结构放置好,并创建相应的 __init__.py
  3. 在项目根目录下打开终端(激活虚拟环境)。
  4. 执行:
python testRunner.py
  1. 执行完成后,浏览器会自动打开 Allure 报告,并显示测试结果。

六、框架亮点与最佳实践

1. 分层设计

  • page:存放元素定位表达式,与具体操作分离,页面元素变动只需修改此处。
  • business:封装业务逻辑(如登录),提高复用性。
  • test_case:只关注测试步骤与断言,通过调用 business 和 operateElement 完成。

2. 操作封装(OperateElement)

  • 将 Selenium 原生方法包装为 clicksend_keys 等,内置显式等待,避免手动加 time.sleep
  • 异常时自动截图并附加到 Allure 报告中,方便定位问题。
  • 每个步骤都生成 Allure 步骤,报告阅读性极佳。

3. 报告与历史数据聚合

  • testRunner.py 自动执行所有测试用例,生成带时间戳的原始数据目录,保证每次报告独立。
  • 通过 allure-history 目录保存历史趋势,报告中可看到测试结果变化曲线。
  • 动态生成 environment.properties,展示测试环境信息。

4. 可扩展性

  • 可在 operateElement.py 中增加更多通用方法(如 select_by_valuescroll_to_element 等)。
  • 页面对象可使用更稳定的定位策略(如 iddata-testid),提高脚本健壮性。
  • 可集成数据驱动(pytest 的 @pytest.mark.parametrize 或读取 Excel/JSON)。

七、注意事项与改进建议

  1. 定位器稳定性
    当前很多 XPath 是绝对路径,页面结构变化会导致脚本失败。建议与开发约定使用 iddata-testid 等稳定属性。

  2. 显式等待
    虽然 OperateElement 已封装等待,但用例中仍有 time.sleep,建议替换为更智能的等待条件(如等待元素消失、等待文本出现)。

  3. 无头模式
    test_projectManagement.py 中已开启 --headless,可在 CI 环境中无界面运行,需确保浏览器驱动支持。

  4. 日志记录
    除 allure 截图外,可增加 Python logging 模块记录执行日志,便于调试。


八、总结

通过本文,我们完整搭建了一套企业级 UI 自动化测试框架,实现了从元素定位、操作封装、业务逻辑到测试用例、报告生成的全流程。所有代码开箱即用,你可以直接复制并应用到自己的项目中。后续还可以集成 Jenkins、GitLab CI,实现持续测试。

这套框架已经在多个企业项目中落地,有效提升了测试效率与反馈质量。赶紧动手试试吧!


最后:如果觉得本文对你有帮助,欢迎点赞、收藏、分享。有任何问题或建议,可以在评论区留言交流!

Logo

这里是“一人公司”的成长家园。我们提供从产品曝光、技术变现到法律财税的全栈内容,并连接云服务、办公空间等稀缺资源,助你专注创造,无忧运营。

更多推荐