1. 项目概述为什么我们需要一个自己的WebUI自动化测试框架如果你是一名测试工程师或者正在向这个方向转型那么“WebUI自动化测试”这个词对你来说一定不陌生。每天我们可能都在和Selenium、Playwright、Cypress这些工具打交道写脚本、跑用例、看报告。但不知道你有没有遇到过这样的困境团队里每个人写的脚本风格迥异维护起来像在解谜环境一变脚本就大面积报错排查起来耗时耗力或者想加个邮件通知、生成一份漂亮的报告却发现要东拼西凑一堆代码。这时候一个统一、健壮、可扩展的WebUI自动化测试框架就不再是“锦上添花”而是“雪中送炭”的必需品了。简单来说一个WebUI自动化测试框架就是一套约定俗成的规则、工具和最佳实践的集合。它不是为了替代Selenium这类底层驱动工具而是站在它们的肩膀上解决更高层次的问题如何让自动化测试更高效、更稳定、更易于协作。它通常封装了浏览器驱动管理、元素定位、测试数据管理、用例组织、报告生成和异常处理等通用能力。想象一下如果没有框架每次写测试就像从零开始造轮子而有了框架你拿到手的是一辆已经组装好的自行车你只需要专注在“骑去哪里”即业务测试逻辑上。这个项目就是带你从零开始设计和搭建一个属于你自己或团队的、贴合实际需求的WebUI自动化测试框架。我们将以最主流的Python Selenium技术栈为基础因为它生态成熟、学习资源丰富但框架的设计思想是通用的同样适用于Playwright或Pytest。我们会深入每个模块的“为什么”和“怎么做”让你不仅会搭更懂其然和所以然。无论你是想提升个人技术深度还是为团队解决自动化测试的痛点这篇文章都将提供一条清晰的路径和大量可直接复用的代码。2. 框架核心设计与架构选型在动手写第一行代码之前我们必须想清楚框架要解决的核心问题以及如何组织代码。一个混乱的框架比没有框架更可怕。这里我推荐采用经典的“分层设计”与“Page Object Model (POM页面对象模式)”相结合的模式这是经过无数项目验证的最佳实践。2.1 为什么选择分层设计与POM模式分层设计的核心思想是“分离关注点”。我们将框架分为不同的层次每层只负责一件事层与层之间通过清晰的接口通信。这样做的好处是高可维护性当Web页面UI发生变化时你通常只需要修改页面对象层Page Layer的元素定位符而不需要改动大量的测试用例脚本。高可读性测试用例Test Case Layer读起来就像是在描述业务场景例如login_page.login(“admin”, “123456”)而不是一堆find_element_by_id的技术细节。高复用性封装好的通用操作如等待、截图和页面对象可以在多个测试用例中被重复使用。POM模式是分层设计在UI自动化中的具体体现。它将每个Web页面抽象成一个类Page Class页面的元素定位和基本操作封装成这个类的方法。测试用例则通过调用这些页面对象的方法来组合成完整的业务流。基于这些原则我建议的框架目录结构如下your_automation_framework/ ├── configs/ # 配置文件目录 │ ├── config.ini # 主配置文件数据库、URL、日志级别等 │ └── browser_config.json # 浏览器特定配置窗口大小、无头模式等 ├── drivers/ # 浏览器驱动存放目录chromedriver, geckodriver ├── logs/ # 运行时日志目录 ├── reports/ # 测试报告输出目录 ├── test_data/ # 测试数据文件JSON, Excel, YAML等 ├── src/ # 框架核心源代码 │ ├── base/ # 基础层 │ │ ├── __init__.py │ │ ├── base_page.py # 所有页面对象的基类 │ │ └── web_driver.py # 浏览器驱动单例管理类核心 │ ├── pages/ # 页面对象层 │ │ ├── __init__.py │ │ ├── login_page.py # 登录页面 │ │ └── home_page.py # 主页 │ ├── utils/ # 工具层 │ │ ├── __init__.py │ │ ├── logger.py # 日志记录模块 │ │ ├── config_reader.py # 配置读取模块 │ │ └── common_actions.py # 通用操作封装如滚动、切换窗口 │ └── assertions/ # 断言层可选封装常用断言 │ └── __init__.py └── tests/ # 测试用例层 ├── __init__.py ├── conftest.py # Pytest的共享fixture配置 ├── test_login.py # 登录测试用例 └── test_search.py # 搜索测试用例这个结构清晰地区分了配置、资源、核心代码和测试用例。接下来我们深入最关键的几个模块。2.2 驱动管理为什么必须用单例模式浏览器驱动WebDriver的初始化和管理是框架稳定性的基石。一个常见的坑是同时打开多个浏览器实例导致资源耗尽或者用例间驱动对象传递混乱。单例模式在这里是完美的解决方案它确保在整个测试运行过程中对于同一种浏览器只有一个驱动实例存在。在src/base/web_driver.py中我们可以这样实现import threading from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager # 推荐使用自动管理驱动版本 from src.utils.config_reader import ConfigReader from src.utils.logger import Logger class WebDriverSingleton: _instance None _lock threading.Lock() # 线程锁防止多线程下创建多个实例 _driver None def __new__(cls): with cls._lock: if cls._instance is None: cls._instance super(WebDriverSingleton, cls).__new__(cls) cls._instance.logger Logger.get_logger(__name__) cls._instance._init_driver() return cls._instance def _init_driver(self): 根据配置初始化浏览器驱动 config ConfigReader() browser_name config.get_browser().lower() self.logger.info(f正在初始化 {browser_name} 浏览器驱动...) if browser_name chrome: options webdriver.ChromeOptions() # 读取配置例如是否无头模式 if config.get_headless(): options.add_argument(--headless) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--window-size1920,1080) # 使用webdriver-manager自动下载和管理匹配的chromedriver try: service ChromeService(ChromeDriverManager().install()) self._driver webdriver.Chrome(serviceservice, optionsoptions) except Exception as e: self.logger.error(fChrome驱动初始化失败: {e}) raise # 可以扩展Firefox, Edge等 elif browser_name firefox: # ... 类似初始化逻辑 pass else: raise ValueError(f不支持的浏览器类型: {browser_name}) self._driver.implicitly_wait(config.get_implicit_wait()) # 隐式等待 self._driver.maximize_window() self.logger.info(f{browser_name} 浏览器驱动初始化成功。) classmethod def get_driver(cls): 获取驱动实例 instance cls() return instance._driver classmethod def quit_driver(cls): 退出驱动清理资源 instance cls._instance if instance and instance._driver: instance.logger.info(正在退出浏览器驱动...) instance._driver.quit() instance._driver None cls._instance None实操心得强烈推荐使用webdriver-manager库。它解决了手动下载、匹配Chrome浏览器与chromedriver版本的噩梦。你不再需要将驱动文件放入drivers/目录并手动更新该库会自动处理。这是提升框架可移植性和维护性的一个关键细节。2.3 配置管理如何让框架灵活适应不同环境测试框架经常需要在不同环境开发、测试、生产下运行配置硬编码在代码里是灾难。我们将配置外置通常使用configparser读取.ini文件或者使用json、yaml。configs/config.ini示例[ENVIRONMENT] base_url https://www.your-test-site.com username test_user password test_pass123 [BROWSER] browser chrome headless false implicit_wait 10 [REPORT] report_title 自动化测试报告 tester_name Your_Name对应的src/utils/config_reader.pyimport os import configparser from pathlib import Path class ConfigReader: _instance None def __new__(cls): if cls._instance is None: cls._instance super(ConfigReader, cls).__new__(cls) cls._instance.config configparser.ConfigParser() config_path Path(__file__).parent.parent.parent / configs / config.ini cls._instance.config.read(config_path, encodingutf-8) return cls._instance def get_base_url(self): return self.config.get(ENVIRONMENT, base_url) def get_browser(self): return self.config.get(BROWSER, browser) # ... 其他get方法这样当需要切换测试环境时只需修改配置文件或者通过命令行参数覆盖配置无需改动代码。3. 核心模块实现与封装艺术有了稳固的基础架构我们来填充血肉实现那些让测试脚本变得优雅和强大的核心模块。3.1 页面对象基类封装所有页面的共性操作所有具体的页面类如LoginPage都应继承自一个基类。这个基类封装了与WebDriver交互的最常用操作并统一了日志、等待和异常处理。这是减少代码重复的关键。src/base/base_page.py核心内容from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException from src.utils.logger import Logger import allure # 如果集成Allure报告 class BasePage: def __init__(self, driver): self.driver driver self.logger Logger.get_logger(self.__class__.__name__) self.wait WebDriverWait(self.driver, timeout10, poll_frequency0.5) def find_element(self, locator, timeoutNone): 查找单个元素支持显式等待 :param locator: 元组如 (By.ID, username) :param timeout: 自定义等待时间 :return: WebElement 对象 wait_obj self.wait if timeout is None else WebDriverWait(self.driver, timeout) try: self.logger.debug(f正在查找元素: {locator}) element wait_obj.until(EC.presence_of_element_located(locator)) # 高亮元素调试用 self._highlight_element(element) return element except TimeoutException: screenshot_path self.take_screenshot(felement_not_found_{locator[1]}) self.logger.error(f元素查找超时: {locator}) # 可以将截图附加到Allure报告 allure.attach.file(screenshot_path, namef元素未找到-{locator[1]}, attachment_typeallure.attachment_type.PNG) raise def click(self, locator): 点击元素并等待元素可点击 element self.wait.until(EC.element_to_be_clickable(locator)) self._highlight_element(element) element.click() self.logger.info(f点击了元素: {locator}) def input_text(self, locator, text): 清空输入框并输入文本 element self.find_element(locator) element.clear() element.send_keys(text) self.logger.info(f在元素 {locator} 中输入了文本: {text}) def get_text(self, locator): 获取元素文本 element self.find_element(locator) text element.text self.logger.info(f获取到元素 {locator} 的文本: {text}) return text def take_screenshot(self, name): 截图并保存到reports目录 import datetime reports_dir Path(__file__).parent.parent.parent / reports / screenshots reports_dir.mkdir(parentsTrue, exist_okTrue) timestamp datetime.datetime.now().strftime(%Y%m%d_%H%M%S) filepath reports_dir / f{name}_{timestamp}.png self.driver.save_screenshot(str(filepath)) self.logger.info(f截图已保存至: {filepath}) return filepath def _highlight_element(self, element): 高亮显示元素用于调试 try: self.driver.execute_script(arguments[0].style.border3px solid red, element) except Exception: pass注意事项find_element方法中的等待策略至关重要。这里使用了EC.presence_of_element_located元素出现在DOM中对于可点击的元素click方法中又使用了EC.element_to_be_clickable。区分“存在”和“可交互”是写出稳定脚本的关键。隐式等待implicitly_wait作为全局兜底显式等待用于关键操作两者结合使用。3.2 具体页面对象以登录页面为例现在我们可以用清晰、易读的方式定义一个登录页面。src/pages/login_page.pyfrom selenium.webdriver.common.by import By from src.base.base_page import BasePage class LoginPage(BasePage): # 1. 定位器集中管理一目了然 USERNAME_INPUT (By.ID, username) PASSWORD_INPUT (By.ID, password) LOGIN_BUTTON (By.XPATH, //button[typesubmit]) ERROR_MESSAGE (By.CLASS_NAME, alert-error) # 2. 页面URL相对路径 PAGE_URL /login def __init__(self, driver): super().__init__(driver) self.driver.get(self._get_full_url()) def _get_full_url(self): 拼接完整的URL from src.utils.config_reader import ConfigReader base_url ConfigReader().get_base_url() return base_url self.PAGE_URL # 3. 页面行为封装成方法 def enter_username(self, username): self.input_text(self.USERNAME_INPUT, username) return self # 支持链式调用 def enter_password(self, password): self.input_text(self.PASSWORD_INPUT, password) return self def click_login(self): self.click(self.LOGIN_BUTTON) from src.pages.home_page import HomePage # 避免循环导入 return HomePage(self.driver) # 返回下一个页面对象实现流程衔接 def get_error_message(self): 获取登录错误提示信息 try: return self.get_text(self.ERROR_MESSAGE) except NoSuchElementException: return # 4. 业务场景组合方法 def login(self, username, password): 完整的登录业务流 self.logger.info(f执行登录操作用户名: {username}) self.enter_username(username) self.enter_password(password) return self.click_login()这种写法的优势非常明显测试用例中调用login_page.login(“admin”, “123456”)即可完成登录并且能清晰地知道登录页有哪些元素和操作。当登录按钮的ID改变时你只需要修改这个文件中的一个常量。3.3 日志模块测试执行的“黑匣子”没有日志的自动化框架就像在黑暗中调试。一个好的日志模块能记录测试执行的每一步在失败时提供完整的上下文。Python自带的logging模块功能强大足够我们使用。src/utils/logger.py简化版import logging import sys from pathlib import Path class Logger: _loggers {} staticmethod def get_logger(name, levellogging.INFO): if name in Logger._loggers: return Logger._loggers[name] logger logging.getLogger(name) logger.setLevel(level) logger.propagate False # 防止日志重复 # 控制台处理器 console_handler logging.StreamHandler(sys.stdout) console_format logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(message)s) console_handler.setFormatter(console_format) logger.addHandler(console_handler) # 文件处理器 log_dir Path(__file__).parent.parent.parent / logs log_dir.mkdir(exist_okTrue) file_handler logging.FileHandler(log_dir / automation.log, encodingutf-8) file_format logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s) file_handler.setFormatter(file_format) logger.addHandler(file_handler) Logger._loggers[name] logger return logger在框架各处使用self.logger.info(“开始执行登录...”)这样的语句运行后你就能在logs/automation.log和控制台看到清晰的时间线这对排查偶发性问题至关重要。4. 测试用例编写与测试运行管理框架搭建好了最终目的是为了运行测试用例。我们使用pytest作为测试运行器因为它比unittest更灵活、插件生态更丰富如pytest-html,pytest-xdist并行测试allure-pytest生成精美报告。4.1 编写一个健壮的测试用例在tests/test_login.py中import pytest import allure from src.pages.login_page import LoginPage from src.utils.config_reader import ConfigReader allure.feature(登录功能) class TestLogin: pytest.fixture(autouseTrue) def setup(self, driver): # driver 来自 conftest.py self.driver driver self.login_page LoginPage(driver) self.config ConfigReader() allure.story(使用正确凭据登录成功) allure.severity(allure.severity_level.CRITICAL) def test_login_success(self): 测试正常登录流程验证跳转到首页 with allure.step(1. 输入正确的用户名和密码): home_page self.login_page.login( self.config.get_username(), self.config.get_password() ) with allure.step(2. 验证登录成功跳转到首页): # 假设首页有独特的欢迎语元素 welcome_text home_page.get_welcome_text() assert 欢迎 in welcome_text or Dashboard in welcome_text allure.attach(self.driver.get_screenshot_as_png(), name登录成功首页, attachment_typeallure.attachment_type.PNG) allure.story(使用错误密码登录失败) def test_login_failure_wrong_password(self): 测试密码错误时的登录失败场景 with allure.step(1. 输入正确用户名和错误密码): # 注意login方法失败时会停留在LoginPage self.login_page.enter_username(self.config.get_username()) self.login_page.enter_password(wrong_password) self.login_page.click_login() # 这里不会跳转页面 with allure.step(2. 验证页面显示了错误提示信息): error_msg self.login_page.get_error_message() assert error_msg ! assert 密码错误 in error_msg or Invalid in error_msg allure.attach(self.driver.get_screenshot_as_png(), name登录失败提示, attachment_typeallure.attachment_type.PNG)用例清晰描述了测试步骤Allure的step注解让报告更易读断言明确并且充分利用了页面对象。4.2 测试固件Fixture管理conftest.py的妙用pytest的conftest.py文件用于存放整个测试目录共享的 fixture。这是我们管理驱动生命周期和初始清理工作的核心。tests/conftest.pyimport pytest from src.base.web_driver import WebDriverSingleton from src.utils.logger import Logger pytest.fixture(scopesession) def driver(): 会话级别的fixture所有测试用例只启动一次浏览器。 适合测试用例间无状态依赖的场景速度最快。 logger Logger.get_logger(__name__) logger.info( 测试会话开始初始化浏览器驱动 ) driver_instance WebDriverSingleton.get_driver() yield driver_instance logger.info( 测试会话结束退出浏览器驱动 ) WebDriverSingleton.quit_driver() pytest.fixture(scopefunction) def driver_per_test(): 函数级别的fixture每个测试用例都重启浏览器。 适合测试用例需要完全独立环境的场景最稳定但最慢。 logger Logger.get_logger(__name__) logger.info(--- 开始单个测试用例初始化浏览器 ---) driver_instance WebDriverSingleton.get_driver() yield driver_instance logger.info(--- 结束单个测试用例清理浏览器 ---) # 注意如果使用单例这里不能quit否则会影响其他用例。 # 更常见的做法是每个用例清理cookies或者不使用单例模式每个用例独立实例。 # driver_instance.delete_all_cookies() # driver_instance.get(about:blank) # 跳转到空白页 pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): Hook函数用于在测试失败时自动截图。 这是pytest的高级用法能极大提升调试效率。 outcome yield report outcome.get_result() if report.when call and report.failed: # 尝试获取driver fixture driver_fixture item.funcargs.get(driver, None) if driver_fixture: allure.attach(driver_fixture.get_screenshot_as_png(), name失败截图, attachment_typeallure.attachment_type.PNG)你可以根据项目需求选择scope“session”快速或scope“function”稳定。pytest_runtest_makereport这个钩子函数是黄金技巧它能在任何测试失败时自动截图并附加到Allure报告中省去了你在每个断言后手动截图的麻烦。5. 报告生成与持续集成初探测试跑完了结果呢一份清晰、直观的报告是自动化测试价值的直接体现。5.1 生成Allure测试报告Allure报告是目前最强大、最美观的测试报告框架之一。安装pip install allure-pytest运行测试并收集结果在项目根目录执行pytest tests/ -v --alluredir./reports/allure-results生成HTML报告执行allure serve ./reports/allure-results会启动一个本地服务并打开报告。Allure报告会展示测试套件、用例层级、步骤详情、截图、日志链接甚至支持显示测试的历史趋势专业度瞬间拉满。5.2 集成到CI/CD流水线框架的最终归宿是集成到持续集成/持续部署CI/CD流程中如Jenkins、GitLab CI、GitHub Actions。核心步骤通常包括代码检出从版本库拉取最新的测试代码和框架。环境准备安装Python依赖 (pip install -r requirements.txt)。执行测试以无头模式运行测试命令例如pytest tests/ --headless --alluredir./reports/allure-results生成报告使用Allure命令行工具生成报告并归档或发布到指定位置。通知根据测试结果通过率决定是否发送邮件或钉钉/企业微信通知。在GitHub Actions中一个简单的.github/workflows/test.yml可能长这样name: WebUI Automation Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.9 - name: Install dependencies run: | pip install -r requirements.txt - name: Install Chrome and ChromeDriver run: | sudo apt-get update sudo apt-get install -y google-chrome-stable - name: Run Tests with Allure run: | pytest tests/ -v --headless --alluredir./reports/allure-results - name: Generate Allure Report uses: simple-elf/allure-report-actionmaster if: always() with: allure_results: ./reports/allure-results allure_report: ./reports/allure-report keep_reports: 5 - name: Upload Allure Report uses: actions/upload-artifactv3 if: always() with: name: allure-report path: ./reports/allure-report6. 常见问题排查与进阶优化在实际使用中你一定会遇到各种“坑”。这里记录一些典型问题和我的解决方案。6.1 元素定位失败自动化测试的头号敌人问题NoSuchElementException,ElementNotInteractableException等。排查思路等待策略不足这是最常见原因。确保使用了合适的显式等待WebDriverWait而不仅仅是隐式等待。对于动态加载的元素可以等待其可见、可点击或具有特定属性。iframe/Shadow DOM如果元素在 iframe 或 Shadow DOM 内部必须先切换到对应的上下文。# 切换iframe iframe driver.find_element(By.TAG_NAME, “iframe”) driver.switch_to.frame(iframe) # 操作iframe内元素... driver.switch_to.default_content() # 切回来XPath/CSS Selector不稳定避免使用绝对路径或依赖页面结构的复杂表达式。优先使用ID、Name等稳定属性。与前端开发约定为关键测试元素添加>
从零构建WebUI自动化测试框架:Python+Selenium+POM分层设计实战
1. 项目概述为什么我们需要一个自己的WebUI自动化测试框架如果你是一名测试工程师或者正在向这个方向转型那么“WebUI自动化测试”这个词对你来说一定不陌生。每天我们可能都在和Selenium、Playwright、Cypress这些工具打交道写脚本、跑用例、看报告。但不知道你有没有遇到过这样的困境团队里每个人写的脚本风格迥异维护起来像在解谜环境一变脚本就大面积报错排查起来耗时耗力或者想加个邮件通知、生成一份漂亮的报告却发现要东拼西凑一堆代码。这时候一个统一、健壮、可扩展的WebUI自动化测试框架就不再是“锦上添花”而是“雪中送炭”的必需品了。简单来说一个WebUI自动化测试框架就是一套约定俗成的规则、工具和最佳实践的集合。它不是为了替代Selenium这类底层驱动工具而是站在它们的肩膀上解决更高层次的问题如何让自动化测试更高效、更稳定、更易于协作。它通常封装了浏览器驱动管理、元素定位、测试数据管理、用例组织、报告生成和异常处理等通用能力。想象一下如果没有框架每次写测试就像从零开始造轮子而有了框架你拿到手的是一辆已经组装好的自行车你只需要专注在“骑去哪里”即业务测试逻辑上。这个项目就是带你从零开始设计和搭建一个属于你自己或团队的、贴合实际需求的WebUI自动化测试框架。我们将以最主流的Python Selenium技术栈为基础因为它生态成熟、学习资源丰富但框架的设计思想是通用的同样适用于Playwright或Pytest。我们会深入每个模块的“为什么”和“怎么做”让你不仅会搭更懂其然和所以然。无论你是想提升个人技术深度还是为团队解决自动化测试的痛点这篇文章都将提供一条清晰的路径和大量可直接复用的代码。2. 框架核心设计与架构选型在动手写第一行代码之前我们必须想清楚框架要解决的核心问题以及如何组织代码。一个混乱的框架比没有框架更可怕。这里我推荐采用经典的“分层设计”与“Page Object Model (POM页面对象模式)”相结合的模式这是经过无数项目验证的最佳实践。2.1 为什么选择分层设计与POM模式分层设计的核心思想是“分离关注点”。我们将框架分为不同的层次每层只负责一件事层与层之间通过清晰的接口通信。这样做的好处是高可维护性当Web页面UI发生变化时你通常只需要修改页面对象层Page Layer的元素定位符而不需要改动大量的测试用例脚本。高可读性测试用例Test Case Layer读起来就像是在描述业务场景例如login_page.login(“admin”, “123456”)而不是一堆find_element_by_id的技术细节。高复用性封装好的通用操作如等待、截图和页面对象可以在多个测试用例中被重复使用。POM模式是分层设计在UI自动化中的具体体现。它将每个Web页面抽象成一个类Page Class页面的元素定位和基本操作封装成这个类的方法。测试用例则通过调用这些页面对象的方法来组合成完整的业务流。基于这些原则我建议的框架目录结构如下your_automation_framework/ ├── configs/ # 配置文件目录 │ ├── config.ini # 主配置文件数据库、URL、日志级别等 │ └── browser_config.json # 浏览器特定配置窗口大小、无头模式等 ├── drivers/ # 浏览器驱动存放目录chromedriver, geckodriver ├── logs/ # 运行时日志目录 ├── reports/ # 测试报告输出目录 ├── test_data/ # 测试数据文件JSON, Excel, YAML等 ├── src/ # 框架核心源代码 │ ├── base/ # 基础层 │ │ ├── __init__.py │ │ ├── base_page.py # 所有页面对象的基类 │ │ └── web_driver.py # 浏览器驱动单例管理类核心 │ ├── pages/ # 页面对象层 │ │ ├── __init__.py │ │ ├── login_page.py # 登录页面 │ │ └── home_page.py # 主页 │ ├── utils/ # 工具层 │ │ ├── __init__.py │ │ ├── logger.py # 日志记录模块 │ │ ├── config_reader.py # 配置读取模块 │ │ └── common_actions.py # 通用操作封装如滚动、切换窗口 │ └── assertions/ # 断言层可选封装常用断言 │ └── __init__.py └── tests/ # 测试用例层 ├── __init__.py ├── conftest.py # Pytest的共享fixture配置 ├── test_login.py # 登录测试用例 └── test_search.py # 搜索测试用例这个结构清晰地区分了配置、资源、核心代码和测试用例。接下来我们深入最关键的几个模块。2.2 驱动管理为什么必须用单例模式浏览器驱动WebDriver的初始化和管理是框架稳定性的基石。一个常见的坑是同时打开多个浏览器实例导致资源耗尽或者用例间驱动对象传递混乱。单例模式在这里是完美的解决方案它确保在整个测试运行过程中对于同一种浏览器只有一个驱动实例存在。在src/base/web_driver.py中我们可以这样实现import threading from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from webdriver_manager.chrome import ChromeDriverManager # 推荐使用自动管理驱动版本 from src.utils.config_reader import ConfigReader from src.utils.logger import Logger class WebDriverSingleton: _instance None _lock threading.Lock() # 线程锁防止多线程下创建多个实例 _driver None def __new__(cls): with cls._lock: if cls._instance is None: cls._instance super(WebDriverSingleton, cls).__new__(cls) cls._instance.logger Logger.get_logger(__name__) cls._instance._init_driver() return cls._instance def _init_driver(self): 根据配置初始化浏览器驱动 config ConfigReader() browser_name config.get_browser().lower() self.logger.info(f正在初始化 {browser_name} 浏览器驱动...) if browser_name chrome: options webdriver.ChromeOptions() # 读取配置例如是否无头模式 if config.get_headless(): options.add_argument(--headless) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--window-size1920,1080) # 使用webdriver-manager自动下载和管理匹配的chromedriver try: service ChromeService(ChromeDriverManager().install()) self._driver webdriver.Chrome(serviceservice, optionsoptions) except Exception as e: self.logger.error(fChrome驱动初始化失败: {e}) raise # 可以扩展Firefox, Edge等 elif browser_name firefox: # ... 类似初始化逻辑 pass else: raise ValueError(f不支持的浏览器类型: {browser_name}) self._driver.implicitly_wait(config.get_implicit_wait()) # 隐式等待 self._driver.maximize_window() self.logger.info(f{browser_name} 浏览器驱动初始化成功。) classmethod def get_driver(cls): 获取驱动实例 instance cls() return instance._driver classmethod def quit_driver(cls): 退出驱动清理资源 instance cls._instance if instance and instance._driver: instance.logger.info(正在退出浏览器驱动...) instance._driver.quit() instance._driver None cls._instance None实操心得强烈推荐使用webdriver-manager库。它解决了手动下载、匹配Chrome浏览器与chromedriver版本的噩梦。你不再需要将驱动文件放入drivers/目录并手动更新该库会自动处理。这是提升框架可移植性和维护性的一个关键细节。2.3 配置管理如何让框架灵活适应不同环境测试框架经常需要在不同环境开发、测试、生产下运行配置硬编码在代码里是灾难。我们将配置外置通常使用configparser读取.ini文件或者使用json、yaml。configs/config.ini示例[ENVIRONMENT] base_url https://www.your-test-site.com username test_user password test_pass123 [BROWSER] browser chrome headless false implicit_wait 10 [REPORT] report_title 自动化测试报告 tester_name Your_Name对应的src/utils/config_reader.pyimport os import configparser from pathlib import Path class ConfigReader: _instance None def __new__(cls): if cls._instance is None: cls._instance super(ConfigReader, cls).__new__(cls) cls._instance.config configparser.ConfigParser() config_path Path(__file__).parent.parent.parent / configs / config.ini cls._instance.config.read(config_path, encodingutf-8) return cls._instance def get_base_url(self): return self.config.get(ENVIRONMENT, base_url) def get_browser(self): return self.config.get(BROWSER, browser) # ... 其他get方法这样当需要切换测试环境时只需修改配置文件或者通过命令行参数覆盖配置无需改动代码。3. 核心模块实现与封装艺术有了稳固的基础架构我们来填充血肉实现那些让测试脚本变得优雅和强大的核心模块。3.1 页面对象基类封装所有页面的共性操作所有具体的页面类如LoginPage都应继承自一个基类。这个基类封装了与WebDriver交互的最常用操作并统一了日志、等待和异常处理。这是减少代码重复的关键。src/base/base_page.py核心内容from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException from src.utils.logger import Logger import allure # 如果集成Allure报告 class BasePage: def __init__(self, driver): self.driver driver self.logger Logger.get_logger(self.__class__.__name__) self.wait WebDriverWait(self.driver, timeout10, poll_frequency0.5) def find_element(self, locator, timeoutNone): 查找单个元素支持显式等待 :param locator: 元组如 (By.ID, username) :param timeout: 自定义等待时间 :return: WebElement 对象 wait_obj self.wait if timeout is None else WebDriverWait(self.driver, timeout) try: self.logger.debug(f正在查找元素: {locator}) element wait_obj.until(EC.presence_of_element_located(locator)) # 高亮元素调试用 self._highlight_element(element) return element except TimeoutException: screenshot_path self.take_screenshot(felement_not_found_{locator[1]}) self.logger.error(f元素查找超时: {locator}) # 可以将截图附加到Allure报告 allure.attach.file(screenshot_path, namef元素未找到-{locator[1]}, attachment_typeallure.attachment_type.PNG) raise def click(self, locator): 点击元素并等待元素可点击 element self.wait.until(EC.element_to_be_clickable(locator)) self._highlight_element(element) element.click() self.logger.info(f点击了元素: {locator}) def input_text(self, locator, text): 清空输入框并输入文本 element self.find_element(locator) element.clear() element.send_keys(text) self.logger.info(f在元素 {locator} 中输入了文本: {text}) def get_text(self, locator): 获取元素文本 element self.find_element(locator) text element.text self.logger.info(f获取到元素 {locator} 的文本: {text}) return text def take_screenshot(self, name): 截图并保存到reports目录 import datetime reports_dir Path(__file__).parent.parent.parent / reports / screenshots reports_dir.mkdir(parentsTrue, exist_okTrue) timestamp datetime.datetime.now().strftime(%Y%m%d_%H%M%S) filepath reports_dir / f{name}_{timestamp}.png self.driver.save_screenshot(str(filepath)) self.logger.info(f截图已保存至: {filepath}) return filepath def _highlight_element(self, element): 高亮显示元素用于调试 try: self.driver.execute_script(arguments[0].style.border3px solid red, element) except Exception: pass注意事项find_element方法中的等待策略至关重要。这里使用了EC.presence_of_element_located元素出现在DOM中对于可点击的元素click方法中又使用了EC.element_to_be_clickable。区分“存在”和“可交互”是写出稳定脚本的关键。隐式等待implicitly_wait作为全局兜底显式等待用于关键操作两者结合使用。3.2 具体页面对象以登录页面为例现在我们可以用清晰、易读的方式定义一个登录页面。src/pages/login_page.pyfrom selenium.webdriver.common.by import By from src.base.base_page import BasePage class LoginPage(BasePage): # 1. 定位器集中管理一目了然 USERNAME_INPUT (By.ID, username) PASSWORD_INPUT (By.ID, password) LOGIN_BUTTON (By.XPATH, //button[typesubmit]) ERROR_MESSAGE (By.CLASS_NAME, alert-error) # 2. 页面URL相对路径 PAGE_URL /login def __init__(self, driver): super().__init__(driver) self.driver.get(self._get_full_url()) def _get_full_url(self): 拼接完整的URL from src.utils.config_reader import ConfigReader base_url ConfigReader().get_base_url() return base_url self.PAGE_URL # 3. 页面行为封装成方法 def enter_username(self, username): self.input_text(self.USERNAME_INPUT, username) return self # 支持链式调用 def enter_password(self, password): self.input_text(self.PASSWORD_INPUT, password) return self def click_login(self): self.click(self.LOGIN_BUTTON) from src.pages.home_page import HomePage # 避免循环导入 return HomePage(self.driver) # 返回下一个页面对象实现流程衔接 def get_error_message(self): 获取登录错误提示信息 try: return self.get_text(self.ERROR_MESSAGE) except NoSuchElementException: return # 4. 业务场景组合方法 def login(self, username, password): 完整的登录业务流 self.logger.info(f执行登录操作用户名: {username}) self.enter_username(username) self.enter_password(password) return self.click_login()这种写法的优势非常明显测试用例中调用login_page.login(“admin”, “123456”)即可完成登录并且能清晰地知道登录页有哪些元素和操作。当登录按钮的ID改变时你只需要修改这个文件中的一个常量。3.3 日志模块测试执行的“黑匣子”没有日志的自动化框架就像在黑暗中调试。一个好的日志模块能记录测试执行的每一步在失败时提供完整的上下文。Python自带的logging模块功能强大足够我们使用。src/utils/logger.py简化版import logging import sys from pathlib import Path class Logger: _loggers {} staticmethod def get_logger(name, levellogging.INFO): if name in Logger._loggers: return Logger._loggers[name] logger logging.getLogger(name) logger.setLevel(level) logger.propagate False # 防止日志重复 # 控制台处理器 console_handler logging.StreamHandler(sys.stdout) console_format logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - %(message)s) console_handler.setFormatter(console_format) logger.addHandler(console_handler) # 文件处理器 log_dir Path(__file__).parent.parent.parent / logs log_dir.mkdir(exist_okTrue) file_handler logging.FileHandler(log_dir / automation.log, encodingutf-8) file_format logging.Formatter(%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s) file_handler.setFormatter(file_format) logger.addHandler(file_handler) Logger._loggers[name] logger return logger在框架各处使用self.logger.info(“开始执行登录...”)这样的语句运行后你就能在logs/automation.log和控制台看到清晰的时间线这对排查偶发性问题至关重要。4. 测试用例编写与测试运行管理框架搭建好了最终目的是为了运行测试用例。我们使用pytest作为测试运行器因为它比unittest更灵活、插件生态更丰富如pytest-html,pytest-xdist并行测试allure-pytest生成精美报告。4.1 编写一个健壮的测试用例在tests/test_login.py中import pytest import allure from src.pages.login_page import LoginPage from src.utils.config_reader import ConfigReader allure.feature(登录功能) class TestLogin: pytest.fixture(autouseTrue) def setup(self, driver): # driver 来自 conftest.py self.driver driver self.login_page LoginPage(driver) self.config ConfigReader() allure.story(使用正确凭据登录成功) allure.severity(allure.severity_level.CRITICAL) def test_login_success(self): 测试正常登录流程验证跳转到首页 with allure.step(1. 输入正确的用户名和密码): home_page self.login_page.login( self.config.get_username(), self.config.get_password() ) with allure.step(2. 验证登录成功跳转到首页): # 假设首页有独特的欢迎语元素 welcome_text home_page.get_welcome_text() assert 欢迎 in welcome_text or Dashboard in welcome_text allure.attach(self.driver.get_screenshot_as_png(), name登录成功首页, attachment_typeallure.attachment_type.PNG) allure.story(使用错误密码登录失败) def test_login_failure_wrong_password(self): 测试密码错误时的登录失败场景 with allure.step(1. 输入正确用户名和错误密码): # 注意login方法失败时会停留在LoginPage self.login_page.enter_username(self.config.get_username()) self.login_page.enter_password(wrong_password) self.login_page.click_login() # 这里不会跳转页面 with allure.step(2. 验证页面显示了错误提示信息): error_msg self.login_page.get_error_message() assert error_msg ! assert 密码错误 in error_msg or Invalid in error_msg allure.attach(self.driver.get_screenshot_as_png(), name登录失败提示, attachment_typeallure.attachment_type.PNG)用例清晰描述了测试步骤Allure的step注解让报告更易读断言明确并且充分利用了页面对象。4.2 测试固件Fixture管理conftest.py的妙用pytest的conftest.py文件用于存放整个测试目录共享的 fixture。这是我们管理驱动生命周期和初始清理工作的核心。tests/conftest.pyimport pytest from src.base.web_driver import WebDriverSingleton from src.utils.logger import Logger pytest.fixture(scopesession) def driver(): 会话级别的fixture所有测试用例只启动一次浏览器。 适合测试用例间无状态依赖的场景速度最快。 logger Logger.get_logger(__name__) logger.info( 测试会话开始初始化浏览器驱动 ) driver_instance WebDriverSingleton.get_driver() yield driver_instance logger.info( 测试会话结束退出浏览器驱动 ) WebDriverSingleton.quit_driver() pytest.fixture(scopefunction) def driver_per_test(): 函数级别的fixture每个测试用例都重启浏览器。 适合测试用例需要完全独立环境的场景最稳定但最慢。 logger Logger.get_logger(__name__) logger.info(--- 开始单个测试用例初始化浏览器 ---) driver_instance WebDriverSingleton.get_driver() yield driver_instance logger.info(--- 结束单个测试用例清理浏览器 ---) # 注意如果使用单例这里不能quit否则会影响其他用例。 # 更常见的做法是每个用例清理cookies或者不使用单例模式每个用例独立实例。 # driver_instance.delete_all_cookies() # driver_instance.get(about:blank) # 跳转到空白页 pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): Hook函数用于在测试失败时自动截图。 这是pytest的高级用法能极大提升调试效率。 outcome yield report outcome.get_result() if report.when call and report.failed: # 尝试获取driver fixture driver_fixture item.funcargs.get(driver, None) if driver_fixture: allure.attach(driver_fixture.get_screenshot_as_png(), name失败截图, attachment_typeallure.attachment_type.PNG)你可以根据项目需求选择scope“session”快速或scope“function”稳定。pytest_runtest_makereport这个钩子函数是黄金技巧它能在任何测试失败时自动截图并附加到Allure报告中省去了你在每个断言后手动截图的麻烦。5. 报告生成与持续集成初探测试跑完了结果呢一份清晰、直观的报告是自动化测试价值的直接体现。5.1 生成Allure测试报告Allure报告是目前最强大、最美观的测试报告框架之一。安装pip install allure-pytest运行测试并收集结果在项目根目录执行pytest tests/ -v --alluredir./reports/allure-results生成HTML报告执行allure serve ./reports/allure-results会启动一个本地服务并打开报告。Allure报告会展示测试套件、用例层级、步骤详情、截图、日志链接甚至支持显示测试的历史趋势专业度瞬间拉满。5.2 集成到CI/CD流水线框架的最终归宿是集成到持续集成/持续部署CI/CD流程中如Jenkins、GitLab CI、GitHub Actions。核心步骤通常包括代码检出从版本库拉取最新的测试代码和框架。环境准备安装Python依赖 (pip install -r requirements.txt)。执行测试以无头模式运行测试命令例如pytest tests/ --headless --alluredir./reports/allure-results生成报告使用Allure命令行工具生成报告并归档或发布到指定位置。通知根据测试结果通过率决定是否发送邮件或钉钉/企业微信通知。在GitHub Actions中一个简单的.github/workflows/test.yml可能长这样name: WebUI Automation Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv3 - name: Set up Python uses: actions/setup-pythonv4 with: python-version: 3.9 - name: Install dependencies run: | pip install -r requirements.txt - name: Install Chrome and ChromeDriver run: | sudo apt-get update sudo apt-get install -y google-chrome-stable - name: Run Tests with Allure run: | pytest tests/ -v --headless --alluredir./reports/allure-results - name: Generate Allure Report uses: simple-elf/allure-report-actionmaster if: always() with: allure_results: ./reports/allure-results allure_report: ./reports/allure-report keep_reports: 5 - name: Upload Allure Report uses: actions/upload-artifactv3 if: always() with: name: allure-report path: ./reports/allure-report6. 常见问题排查与进阶优化在实际使用中你一定会遇到各种“坑”。这里记录一些典型问题和我的解决方案。6.1 元素定位失败自动化测试的头号敌人问题NoSuchElementException,ElementNotInteractableException等。排查思路等待策略不足这是最常见原因。确保使用了合适的显式等待WebDriverWait而不仅仅是隐式等待。对于动态加载的元素可以等待其可见、可点击或具有特定属性。iframe/Shadow DOM如果元素在 iframe 或 Shadow DOM 内部必须先切换到对应的上下文。# 切换iframe iframe driver.find_element(By.TAG_NAME, “iframe”) driver.switch_to.frame(iframe) # 操作iframe内元素... driver.switch_to.default_content() # 切回来XPath/CSS Selector不稳定避免使用绝对路径或依赖页面结构的复杂表达式。优先使用ID、Name等稳定属性。与前端开发约定为关键测试元素添加>