从零搭建Python+Selenium自动化测试框架:POM设计、Pytest集成与工程化实践

📅 2026/7/1 21:28:28
从零搭建Python+Selenium自动化测试框架:POM设计、Pytest集成与工程化实践
1. 项目概述为什么我们需要自己的自动化测试框架如果你是一名测试工程师或者正在向这个方向转型你肯定不止一次听过“自动化测试”这个词。它听起来很美好能解放重复劳动、提升回归效率、保证交付质量。但现实往往是团队要么在用一些零散的脚本维护成本高得吓人要么在用一个庞大而笨重的商业工具学习曲线陡峭定制化需求难以满足。这时候一个轻量、可控、贴合自身业务需求的自动化测试框架就成了刚需。这个项目就是带你从零开始用 Python 和 Selenium 搭建一个属于你自己的、可扩展的自动化测试框架。它不是教你写几个孤立的测试脚本而是构建一个完整的工程体系。你会学到如何组织测试用例、如何管理测试数据、如何生成直观的报告、如何处理各种环境依赖以及如何让这个框架在团队中真正用起来。Python 以其简洁和丰富的生态成为首选Selenium 则是 Web 自动化测试领域事实上的标准。通过这个实战你得到的不仅是一套代码更是一套解决测试自动化工程问题的思维和方法。2. 框架整体设计与核心思路拆解2.1 框架设计目标与选型考量在动手写第一行代码之前我们必须想清楚这个框架要达成什么目标。一个合格的自动化测试框架至少应该满足以下几点可维护性代码结构清晰易于修改和扩展、可读性测试用例像文档一样清晰、稳定性能妥善处理网络波动、元素加载等不稳定因素、易用性新人能快速上手编写用例以及可集成性能方便地接入 CI/CD 流水线。基于这些目标我们选择Python Selenium Pytest作为技术栈的核心。Python语法简洁学习成本低拥有极其丰富的第三方库如 requests, pandas, openpyxl能轻松处理测试数据、报告生成等周边任务。Selenium支持所有主流浏览器社区活跃是 Web UI 自动化的基石。虽然近年来 Playwright 和 Cypress 等新工具势头很猛但 Selenium 的成熟度、生态广度以及“一次编写多浏览器运行”的能力使其仍然是构建企业级框架的稳妥选择。Pytest相比于 Python 自带的 unittestPytest 更灵活、功能更强大。它支持丰富的插件如生成 HTML 报告的 pytest-html、控制用例执行顺序的 pytest-ordering、简单的 fixture 机制来管理测试前置和后置条件以及参数化测试能极大提升测试用例的编写效率和可读性。为什么不直接用录屏工具或者 Selenium IDE因为它们生成的脚本通常是线性的、硬编码的缺乏结构化和复用性难以应对复杂的测试场景和持续的维护。我们的框架是要面向工程化的。2.2 框架核心目录结构规划一个清晰的目录结构是框架可维护性的基石。在项目根目录下我通常会这样组织automation_framework/ ├── configs/ # 配置文件目录 │ ├── config.ini # 主配置文件数据库、URL、日志级别等 │ └── browser_config.json # 浏览器特定配置窗口大小、无头模式、下载路径等 ├── data/ # 测试数据目录 │ ├── test_data.xlsx # Excel 存储的测试数据 │ └── api_data.json # JSON 存储的 API 测试数据 ├── logs/ # 日志文件目录.gitignore ├── reports/ # 测试报告目录.gitignore │ └── assets/ # 报告依赖的图片等资源 ├── page_objects/ # 页面对象模型Page Object目录 │ ├── __init__.py │ ├── base_page.py # 所有页面对象的基类 │ ├── login_page.py # 登录页面 │ └── home_page.py # 主页 ├── test_cases/ # 测试用例目录 │ ├── __init__.py │ ├── conftest.py # Pytest 的共享 fixture 配置 │ ├── test_login.py # 登录相关测试用例 │ └── test_search.py # 搜索相关测试用例 ├── utils/ # 工具函数目录 │ ├── __init__.py │ ├── logger.py # 自定义日志模块 │ ├── webdriver_helper.py # WebDriver 封装与工具函数 │ └── data_reader.py # 数据读取工具 ├── conftest.py # 项目根目录的全局 fixture ├── pytest.ini # Pytest 全局配置文件 └── requirements.txt # Python 依赖包列表这个结构将配置、数据、页面对象、用例、工具和输出物清晰分离。page_objects目录是实现Page Object Model (POM)设计模式的关键它将页面的元素定位和操作封装成类让测试用例只关注业务逻辑极大提升了代码的可维护性和复用性。3. 核心模块详解与实操要点3.1 环境搭建与依赖管理第一步是准备好战场。你需要安装 Python推荐 3.8 及以上版本然后使用 pip 安装核心依赖。强烈建议使用虚拟环境如venv来隔离项目依赖。创建并激活虚拟环境# 在项目根目录下 python -m venv venv # Windows venv\Scripts\activate # macOS/Linux source venv/bin/activate安装核心包将以下内容保存到requirements.txt然后执行pip install -r requirements.txt。selenium4.0.0 pytest7.0.0 pytest-html3.0.0 pytest-rerunfailures10.0 # 用于失败重试 webdriver-manager3.0.0 # 自动管理浏览器驱动强烈推荐 openpyxl3.0.0 # 读写 Excel 测试数据 PyYAML6.0 # 读写 YAML 配置文件 allure-pytest2.0.0 # 可选生成更美观的 Allure 报告webdriver-manager是一个神器它能自动下载和匹配对应版本的浏览器驱动ChromeDriver, GeckoDriver等彻底告别手动下载和配置驱动路径的烦恼。IDE 配置使用 VSCode 或 PyCharm。在 VSCode 中确保选择正确的 Python 解释器你的虚拟环境并安装 Python 和 Pytest 相关插件。注意国内网络环境安装包可能较慢可以配置 pip 镜像源例如使用清华源pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple3.2 封装 WebDriver打造稳健的浏览器操作核心直接使用原始的webdriver.Chrome()在复杂项目中会很快变得难以维护。我们需要一个封装类来统一管理浏览器的生命周期、提供增强的查找元素方法、并集成自动等待和日志记录。在utils/webdriver_helper.py中import logging from selenium import webdriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.firefox import GeckoDriverManager class WebDriverHelper: def __init__(self, browserchrome, headlessFalse, implicit_wait10): 初始化 WebDriver。 :param browser: 浏览器类型chrome 或 firefox :param headless: 是否无头模式不显示浏览器界面 :param implicit_wait: 隐式等待时间秒 self.logger logging.getLogger(__name__) self.driver None self.browser browser self.headless headless self._init_driver() self.driver.implicitly_wait(implicit_wait) self.wait WebDriverWait(self.driver, 10) # 显式等待对象 def _init_driver(self): 根据配置初始化 WebDriver 实例。 try: if self.browser.lower() chrome: options webdriver.ChromeOptions() if self.headless: options.add_argument(--headless) options.add_argument(--disable-gpu) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) # 禁止 Chrome 受自动化控制提示避免被某些网站检测 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) # 使用 webdriver-manager 自动管理驱动 service webdriver.ChromeService(ChromeDriverManager().install()) self.driver webdriver.Chrome(serviceservice, optionsoptions) elif self.browser.lower() firefox: options webdriver.FirefoxOptions() if self.headless: options.add_argument(--headless) service webdriver.FirefoxService(GeckoDriverManager().install()) self.driver webdriver.Firefox(serviceservice, optionsoptions) else: raise ValueError(f不支持的浏览器类型: {self.browser}) self.logger.info(f{self.browser} 浏览器启动成功无头模式: {self.headless}) except Exception as e: self.logger.error(f启动 {self.browser} 浏览器失败: {e}) raise def find_element(self, locator_type, locator_value, timeout10): 增强的元素查找方法集成显式等待。 :param locator_type: 定位器类型如 id, xpath, css_selector, name :param locator_value: 定位器值 :param timeout: 超时时间秒 :return: WebElement 对象 locator (locator_type, locator_value) try: element WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) ) self.logger.debug(f找到元素: {locator}) return element except TimeoutException: self.logger.error(f查找元素超时: {locator}) # 这里可以附加截图方便调试 self.take_screenshot(felement_not_found_{locator_type}_{locator_value}) raise NoSuchElementException(f元素未找到: {locator}) def take_screenshot(self, name): 截图并保存到指定路径。 screenshot_path f./logs/screenshot_{name}_{datetime.now().strftime(%Y%m%d_%H%M%S)}.png self.driver.save_screenshot(screenshot_path) self.logger.info(f截图已保存: {screenshot_path}) return screenshot_path def quit(self): 关闭浏览器。 if self.driver: self.driver.quit() self.logger.info(浏览器已关闭)这个封装类提供了几个关键优势1)自动驱动管理无需手动下载2)统一的元素查找内置显式等待更稳定3)灵活的配置支持多浏览器和无头模式4)集成日志和截图便于调试。3.3 实现 Page Object Model (POM)POM 是 UI 自动化测试的黄金设计模式。其核心思想是将每个页面封装成一个类页面上的元素定位器作为类的属性页面操作如输入、点击作为类的方法。测试用例则通过调用这些页面对象的方法来完成业务流。首先在page_objects/base_page.py中定义一个所有页面的基类from utils.webdriver_helper import WebDriverHelper import logging class BasePage: def __init__(self, driver: WebDriverHelper): self.driver driver self.logger logging.getLogger(__name__) def open(self, url): 打开指定URL。 self.driver.get(url) self.logger.info(f打开页面: {url}) # 可以在这里定义一些公共方法比如等待页面加载完成的通用逻辑然后实现具体的页面例如page_objects/login_page.pyfrom page_objects.base_page import BasePage class LoginPage(BasePage): # 元素定位器推荐使用元组存储便于维护 USERNAME_INPUT (id, username) PASSWORD_INPUT (id, password) LOGIN_BUTTON (xpath, //button[typesubmit]) ERROR_MSG (css_selector, .alert-error) def enter_username(self, username): 输入用户名。 elem self.driver.find_element(*self.USERNAME_INPUT) # 解包元组 elem.clear() elem.send_keys(username) self.logger.debug(f输入用户名: {username}) def enter_password(self, password): 输入密码。 elem self.driver.find_element(*self.PASSWORD_INPUT) elem.clear() elem.send_keys(password) self.logger.debug(输入密码) def click_login(self): 点击登录按钮。 elem self.driver.find_element(*self.LOGIN_BUTTON) elem.click() self.logger.debug(点击登录按钮) def get_error_message(self): 获取错误提示信息。 try: elem self.driver.find_element(*self.ERROR_MSG, timeout3) # 短时间等待错误信息 return elem.text except: return None def login(self, username, password): 登录业务流程。 self.enter_username(username) self.enter_password(password) self.click_login()通过 POM当登录页面的输入框 ID 从username改为user时你只需要在一个地方LoginPage类修改USERNAME_INPUT这个定位器所有用到这个输入框的测试用例都无需改动维护成本大大降低。4. 测试用例编写与数据驱动4.1 使用 Pytest 编写结构化测试用例有了页面对象编写测试用例就变得非常清晰。在test_cases/test_login.py中import pytest from page_objects.login_page import LoginPage from utils.data_reader import read_test_data_from_excel class TestLogin: 登录功能测试类。 pytest.fixture(autouseTrue) def setup(self, driver): 每个测试方法执行前的准备工作。 self.driver driver self.login_page LoginPage(self.driver) self.login_page.open(https://your-test-site.com/login) yield # 每个测试方法执行后的清理工作如果需要 # self.driver.delete_all_cookies() pytest.mark.parametrize(username, password, expected, [ (admin, correct_password, success), (admin, wrong_password, invalid_credentials), (, some_password, username_required), (admin, , password_required), ]) def test_login_with_different_inputs(self, username, password, expected): 数据驱动测试使用不同组合测试登录功能。 self.login_page.login(username, password) if expected success: # 验证登录成功例如跳转到首页或出现欢迎信息 assert dashboard in self.driver.current_url else: # 验证出现相应的错误提示 error_msg self.login_page.get_error_message() assert error_msg is not None if expected invalid_credentials: assert 用户名或密码错误 in error_msg # ... 其他错误断言 def test_login_success(self): 正向用例使用正确的凭据登录。 # 测试数据可以来自外部文件 test_data read_test_data_from_excel(data/test_data.xlsx, login_success) self.login_page.login(test_data[username], test_data[password]) # 断言登录成功后的状态 assert self.driver.current_url https://your-test-site.com/dashboard # 或者断言页面包含某个成功元素 welcome_elem self.driver.find_element(css_selector, .welcome-msg) assert test_data[username] in welcome_elem.text这里展示了 Pytest 的几个强大功能pytest.fixture用于管理测试资源如 driverautouseTrue让它在每个测试方法中自动执行pytest.mark.parametrize实现了数据驱动测试用一个测试函数覆盖多组数据清晰的断言使得测试意图一目了然。4.2 测试数据管理硬编码的测试数据是维护的噩梦。我们将数据剥离到外部文件。utils/data_reader.py可以这样写import openpyxl import json import yaml from pathlib import Path def read_test_data_from_excel(file_path, sheet_name, keyNone): 从 Excel 文件读取测试数据。 :param file_path: Excel 文件路径 :param sheet_name: 工作表名 :param key: 可选如果提供返回该键对应的行数据字典 :return: 字典或字典列表 workbook openpyxl.load_workbook(file_path, data_onlyTrue) sheet workbook[sheet_name] # 读取表头 headers [cell.value for cell in next(sheet.iter_rows(min_row1, max_row1))] data [] for row in sheet.iter_rows(min_row2, values_onlyTrue): # 从第二行开始 row_data dict(zip(headers, row)) data.append(row_data) workbook.close() if key and data: # 假设第一列是测试用例的标识键 for item in data: if item[headers[0]] key: return item raise ValueError(f未找到键为 {key} 的测试数据) return data def read_config_from_yaml(file_path): 从 YAML 文件读取配置。 with open(file_path, r, encodingutf-8) as f: return yaml.safe_load(f) # 示例读取 JSON 格式的 API 测试数据 def read_api_data(file_path): with open(file_path, r, encodingutf-8) as f: return json.load(f)在 Excel 中你可以这样组织login工作表TestCaseIDusernamepasswordexpected_resultexpected_messageTC_LOGIN_001admincorrect_pwdsuccessTC_LOGIN_002adminwrong_pwdfail用户名或密码错误TC_LOGIN_003some_pwdfail用户名不能为空这样测试逻辑和数据完全分离。新增测试用例时只需在 Excel 中添加一行无需修改代码。5. 测试执行、报告与高级配置5.1 配置 Pytest 并生成测试报告在项目根目录创建pytest.ini文件这是 Pytest 的主配置文件[pytest] # 指定测试文件的位置和模式 testpaths test_cases python_files test_*.py python_classes Test* python_functions test_* # 添加命令行默认选项 addopts -v # 详细输出 --htmlreports/report.html # 生成 HTML 报告 --self-contained-html # 将 CSS 等嵌入 HTML使报告单文件化 --reruns 1 # 失败用例重试1次 --reruns-delay 2 # 重试间隔2秒 --capturesys # 捕获输出 -l # 显示失败测试的局部变量 # 定义标记用于分类运行测试 markers smoke: 冒烟测试用例 regression: 回归测试用例 slow: 运行较慢的测试用例现在在命令行中运行pytest它会自动按照配置执行test_cases目录下所有test_*.py文件中的测试并生成一份详细的 HTML 报告在reports/report.html。报告里会包含通过/失败数量、执行时间、失败用例的错误信息和截图如果集成了的话非常适合在团队中分享结果。5.2 使用 Fixture 管理测试生命周期conftest.py是 Pytest 的本地插件文件在这里定义的 fixture 可以被同一目录及子目录下的所有测试文件使用。我们在项目根目录和test_cases目录下各放一个实现不同层级的共享。项目根目录的conftest.py负责最核心的资源管理import pytest import logging from utils.webdriver_helper import WebDriverHelper from utils.logger import setup_logger # 设置日志 setup_logger() pytest.fixture(scopesession) def config(): 读取全局配置示例。 # 这里可以从 config.ini 或 YAML 文件读取 return { base_url: https://your-test-site.com, browser: chrome, headless: False, timeout: 10 } pytest.fixture(scopefunction) # 默认是 function 级别每个测试函数一个 driver def driver(config): 最重要的 fixture为每个测试用例提供 WebDriver 实例。 scopefunction 确保每个测试用例都有干净的浏览器会话。 driver_helper None try: driver_helper WebDriverHelper( browserconfig.get(browser, chrome), headlessconfig.get(headless, False) ) driver_helper.driver.maximize_window() yield driver_helper.driver # 将 driver 对象传递给测试用例 finally: # 无论测试成功还是失败最后都会关闭浏览器 if driver_helper: driver_helper.quit() pytest.fixture(scopesession, autouseTrue) def global_setup_teardown(): 会话级别的 setup 和 teardown整个测试会话只执行一次。 logging.info( 开始自动化测试会话 ) yield logging.info( 自动化测试会话结束 )test_cases/conftest.py则可以定义一些测试用例目录特有的 fixture比如登录状态的 fixtureimport pytest from page_objects.login_page import LoginPage pytest.fixture def logged_in_driver(driver): 提供一个已登录状态的 driver。 login_page LoginPage(driver) login_page.open(https://your-test-site.com/login) login_page.login(standard_user, secret_sauce) # 使用一个测试账号 yield driver # 登出清理如果需要 # driver.get(https://your-test-site.com/logout)在测试用例中你只需要在参数中声明需要的 fixturePytest 会自动注入def test_something_with_login(logged_in_driver): # 使用已登录的 driver home_page HomePage(logged_in_driver) # ... 测试逻辑 def test_something_else(driver): # 使用全新的 driver # ... 测试逻辑这种设计让测试用例的编写非常简洁且资源管理井井有条。5.3 日志记录与失败截图良好的日志是调试和追溯问题的生命线。utils/logger.pyimport logging import sys from pathlib import Path from datetime import datetime def setup_logger(name__name__, log_levellogging.INFO): 配置并返回一个日志记录器。 # 创建 logs 目录 log_dir Path(logs) log_dir.mkdir(exist_okTrue) # 生成带时间的日志文件名 log_file log_dir / fautomation_{datetime.now().strftime(%Y%m%d)}.log # 获取 logger logger logging.getLogger(name) logger.setLevel(log_level) # 避免重复添加 handler if logger.handlers: return logger # 格式化器 formatter logging.Formatter( %(asctime)s - %(name)s - %(levelname)s - %(message)s, datefmt%Y-%m-%d %H:%M:%S ) # 文件处理器 file_handler logging.FileHandler(log_file, encodingutf-8) file_handler.setFormatter(formatter) logger.addHandler(file_handler) # 控制台处理器 console_handler logging.StreamHandler(sys.stdout) console_handler.setFormatter(formatter) logger.addHandler(console_handler) return logger在 WebDriver 封装和页面对象中我们都使用这个 logger 来记录关键操作和错误。结合 Pytest 的pytest.hookimpl钩子函数我们可以在测试失败时自动截图。在conftest.py中添加pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): Pytest 钩子在每个测试步骤后生成报告并在失败时截图。 outcome yield report outcome.get_result() if report.when call and report.failed: # 获取测试用例中的 driver fixture driver_fixture item.funcargs.get(driver) if driver_fixture: # 调用我们封装好的截图方法 screenshot_path f./logs/failure_{item.name}_{datetime.now().strftime(%H%M%S)}.png driver_fixture.save_screenshot(screenshot_path) # 将截图路径附加到测试报告中 if hasattr(report, extra): import pytest_html report.extra.append(pytest_html.extras.image(screenshot_path, 失败截图)) report.extra.append(pytest_html.extras.html(fdiv截图: a href{screenshot_path}{screenshot_path}/a/div))这样每次测试失败都会在logs目录下生成一张截图并链接到 HTML 报告中让你能直观地看到失败时的页面状态。6. 常见问题排查与实战技巧6.1 元素定位失败问题精讲这是 Selenium 自动化中最常见的问题没有之一。其根源通常在于“时机不对”或“定位器不对”。动态元素与等待策略问题代码执行速度远快于页面加载或 JavaScript 渲染速度导致在元素出现之前就去查找它。解决永远不要依赖固定的time.sleep()。使用显式等待Explicit Wait。我们封装在WebDriverHelper.find_element中的WebDriverWait就是干这个的。它会在指定时间内不断尝试查找元素直到找到或超时。进阶技巧等待条件不仅仅是presence_of_element_located元素存在于 DOM还有visibility_of_element_located元素可见、element_to_be_clickable元素可点击等。根据你的实际操作选择合适的条件。# 等待元素可见并可点击 from selenium.webdriver.support import expected_conditions as EC element WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, dynamic-button)) ) element.click()定位器不稳定问题使用了绝对 XPath如/html/body/div[3]/div[2]/form/input[1]页面结构微调就会导致定位失败。或者使用了容易变化的 ID/Class如带随机后缀idsubmit-btn-12345。解决优先级idnamecss selectorxpath。ID 通常是唯一且最稳定的。编写稳健的 CSS Selector 和 XPathCSS: 避免使用依赖位置的结构。如input.form-control[nameusername]比div.container form div:nth-child(2) input好得多。XPath: 使用属性、文本内容或相对路径。如//button[contains(class, btn-primary) and text()提交]或//input[placeholder请输入用户名]。与开发约定为关键测试元素添加稳定的>iframe driver.find_element(tag name, iframe) driver.switch_to.frame(iframe) # 现在可以定位 iframe 内的元素了 inner_element driver.find_element(id, inner-elem) # 操作完成后切回主文档 driver.switch_to.default_content()Shadow DOM需要使用execute_script执行 JavaScript 来穿透 Shadow Root。# 假设有一个 shadow host 元素 shadow_host driver.find_element(css selector, custom-element) # 获取其 shadow root shadow_root driver.execute_script(return arguments[0].shadowRoot, shadow_host) # 现在可以在 shadow root 下查找元素 shadow_element shadow_root.find_element(css selector, button)6.2 测试稳定性提升技巧失败重试机制网络抖动、资源加载慢可能导致偶发性失败。使用pytest-rerunfailures插件已在pytest.ini中配置--reruns 1可以让失败的用例自动重试一次提高稳定性但需注意区分“偶发失败”和“真实缺陷”。页面加载状态判断对于单页应用SPA简单的driver.get()后页面可能并未真正就绪。可以等待某个关键元素出现或者等待 JavaScript 的某个变量/状态。# 等待页面标题包含特定文本 WebDriverWait(driver, 10).until(EC.title_contains(Dashboard)) # 等待 jQuery如果使用活动完成 WebDriverWait(driver, 10).until(lambda d: d.execute_script(return jQuery.active 0)) # 等待特定 JS 变量就绪 WebDriverWait(driver, 10).until(lambda d: d.execute_script(return window.appLoaded true))处理弹窗和浏览器通知在启动浏览器时通过Options禁用弹窗或设置默认行为。options webdriver.ChromeOptions() prefs { profile.default_content_setting_values.notifications: 2, # 禁用通知 download.default_directory: /path/to/downloads, # 设置下载路径 } options.add_experimental_option(prefs, prefs)6.3 框架集成与持续运行命令行参数化通过pytest.addoption钩子可以让框架支持命令行参数动态指定浏览器、环境等。# 在 conftest.py 中 def pytest_addoption(parser): parser.addoption(--browser, actionstore, defaultchrome, help浏览器类型: chrome 或 firefox) parser.addoption(--env, actionstore, defaultstaging, help测试环境: staging 或 prod) pytest.fixture(scopesession) def config(request): browser request.config.getoption(--browser) env request.config.getoption(--env) # 根据 env 加载不同的配置文件 env_config load_config_for_env(env) return {browser: browser, **env_config}运行命令pytest --browserfirefox --envprod集成到 CI/CD (Jenkins/GitLab CI)在 CI 服务器上通常以无头模式运行测试。确保你的框架支持--headless参数并且所有依赖包括浏览器都能在无图形界面的服务器上正确安装。一个简单的 GitLab CI.gitlab-ci.yml配置示例stages: - test automation-test: stage: test image: python:3.10-slim before_script: - apt-get update apt-get install -y wget unzip chromium chromium-driver - pip install -r requirements.txt script: - pytest --headless --htmlreport.html artifacts: when: always paths: - reports/ - logs/ reports: junit: reports/junit.xml only: - main - merge_requests测试报告与通知除了 HTML 报告还可以集成 Allure 生成更美观的交互式报告或者将测试结果通过 Webhook 发送到团队聊天工具如钉钉、飞书、Slack。搭建自动化测试框架是一个迭代的过程不要试图一开始就设计得完美无缺。从核心的页面对象和基础用例开始逐步添加日志、报告、数据驱动、CI 集成等模块。最重要的是让框架尽快在真实项目中小范围跑起来在实践中发现问题并持续改进。这个由你亲手搭建、完全可控的框架将成为你提升测试效率和质量最得力的武器。