从 0 到 1 搭建企业级 UI 自动化测试框架(Python + Selenium + Pytest + Allure)
文章目录
一套可以直接落地的自动化测试框架,涵盖页面对象、业务封装、异常截图、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}")
五、如何运行测试?
- 确保 Chrome 浏览器已安装,且 ChromeDriver 已配置在 PATH 中。
- 将上述所有文件按目录结构放置好,并创建相应的
__init__.py。 - 在项目根目录下打开终端(激活虚拟环境)。
- 执行:
python testRunner.py
- 执行完成后,浏览器会自动打开 Allure 报告,并显示测试结果。
六、框架亮点与最佳实践
1. 分层设计
- page:存放元素定位表达式,与具体操作分离,页面元素变动只需修改此处。
- business:封装业务逻辑(如登录),提高复用性。
- test_case:只关注测试步骤与断言,通过调用 business 和 operateElement 完成。
2. 操作封装(OperateElement)
- 将 Selenium 原生方法包装为
click、send_keys等,内置显式等待,避免手动加time.sleep。 - 异常时自动截图并附加到 Allure 报告中,方便定位问题。
- 每个步骤都生成 Allure 步骤,报告阅读性极佳。
3. 报告与历史数据聚合
testRunner.py自动执行所有测试用例,生成带时间戳的原始数据目录,保证每次报告独立。- 通过
allure-history目录保存历史趋势,报告中可看到测试结果变化曲线。 - 动态生成
environment.properties,展示测试环境信息。
4. 可扩展性
- 可在
operateElement.py中增加更多通用方法(如select_by_value、scroll_to_element等)。 - 页面对象可使用更稳定的定位策略(如
id、data-testid),提高脚本健壮性。 - 可集成数据驱动(pytest 的
@pytest.mark.parametrize或读取 Excel/JSON)。
七、注意事项与改进建议
-
定位器稳定性
当前很多 XPath 是绝对路径,页面结构变化会导致脚本失败。建议与开发约定使用id或data-testid等稳定属性。 -
显式等待
虽然OperateElement已封装等待,但用例中仍有time.sleep,建议替换为更智能的等待条件(如等待元素消失、等待文本出现)。 -
无头模式
test_projectManagement.py中已开启--headless,可在 CI 环境中无界面运行,需确保浏览器驱动支持。 -
日志记录
除 allure 截图外,可增加 Python logging 模块记录执行日志,便于调试。
八、总结
通过本文,我们完整搭建了一套企业级 UI 自动化测试框架,实现了从元素定位、操作封装、业务逻辑到测试用例、报告生成的全流程。所有代码开箱即用,你可以直接复制并应用到自己的项目中。后续还可以集成 Jenkins、GitLab CI,实现持续测试。
这套框架已经在多个企业项目中落地,有效提升了测试效率与反馈质量。赶紧动手试试吧!
最后:如果觉得本文对你有帮助,欢迎点赞、收藏、分享。有任何问题或建议,可以在评论区留言交流!
更多推荐



所有评论(0)