ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

从零搭建Python+Selenium自动化测试框架:核心设计与实战指南

2026/8/4 6:40:06 拓冰建站 浏览量
从零搭建Python+Selenium自动化测试框架:核心设计与实战指南 1. 项目概述为什么我们需要一个Web自动化框架如果你和我一样曾经为了测试一个网页表单或者定时抓取某个网站的数据而不得不一遍又一遍地手动点击、输入、刷新那么你一定会对Web自动化产生浓厚的兴趣。那种重复、枯燥且容易出错的工作正是程序应该替我们完成的。Python Selenium的组合就是打开这扇大门的钥匙。Selenium是一个强大的浏览器自动化工具而Python以其简洁的语法和丰富的生态成为了驱动Selenium最流行的语言之一。但仅仅会用driver.find_element_by_id()写几行脚本距离“自动化”还很远。脚本散乱、难以维护、复用性差、错误处理缺失这些都是新手常踩的坑。因此构建一个属于自己的Web自动化框架不是为了炫技而是为了将零散的脚本工程化、模块化让自动化工作真正变得高效、可靠和可持续。这个学习笔记记录了我从零开始基于Python和Selenium搭建一个具备基础框架能力的自动化项目的过程。它不是一个面面俱到的企业级框架而是一个从实战出发包含了核心设计思想、关键代码实现、常见问题踩坑与解决方案的“脚手架”。无论你是想为你的个人项目添加自动化测试能力还是希望系统地学习Web自动化框架的设计这份笔记都能提供一个清晰的路径和可直接复用的代码。我们将从最核心的驱动管理、页面对象模型入手逐步扩展到测试用例组织、日志报告和持续集成思想最终形成一个结构清晰、易于扩展的自动化项目骨架。2. 框架核心设计与思路拆解2.1 框架的目标与边界定义在动手写第一行代码之前明确框架要解决什么问题至关重要。一个通用的Web自动化框架其核心目标可以归纳为以下几点提高脚本可维护性当页面元素发生变化时修改点应该尽可能集中而不是散落在成百上千个测试脚本里。增强脚本稳定性网络延迟、元素加载慢、弹窗干扰……自动化脚本运行环境充满不确定性框架需要提供健壮的等待和异常处理机制。提升编写效率提供通用的基础操作封装如输入、点击、断言让脚本编写者更专注于业务逻辑而非底层API的调用细节。方便测试管理与执行能够灵活地组织、选择、运行测试用例并生成清晰易懂的测试报告。易于集成与扩展能够方便地与持续集成工具如Jenkins、邮件服务、监控系统等对接。基于这些目标我决定采用经典的“页面对象模型”作为框架的核心设计模式并辅以数据驱动和关键字驱动的思想。框架的边界是不试图封装所有Selenium操作而是提供一套模式和基础工具让在此基础上构建的自动化脚本结构良好、易于维护。2.2 技术栈选型与理由核心驱动Python 3.8。选择Python是因为其在自动化领域的绝对主流地位语法简洁社区活跃拥有海量的第三方库支持。3.8版本是一个长期稳定且功能完备的版本。浏览器自动化Selenium 4.x。Selenium是Web自动化的行业标准支持所有主流浏览器。4.x版本相较于3.x提供了更简洁的API如新的find_element方法、更好的W3C标准兼容性以及原生支持CDP协议对于现代浏览器尤其是Chrome/Edge的控制能力更强。测试组织与运行pytest。相比于Python自带的unittestpytest更灵活、更强大。它支持丰富的插件如并行测试、用例依赖管理、更简洁的断言写法直接使用assert、以及强大的fixture机制非常适合构建复杂的测试套件。报告生成allure-pytest。Allure能生成非常美观、信息丰富的交互式测试报告直观展示用例执行情况、步骤详情、截图和日志是提升测试结果可读性的利器。配置管理PyYAML。使用YAML文件来管理配置如浏览器类型、基础URL、超时时间、用户凭证等将易变的数据与代码分离使框架更灵活。日志记录logging。Python标准库的logging模块足够强大可以配置将日志输出到控制台和文件方便调试和问题回溯。这个技术栈组合平衡了功能强大与易于上手是当前Python Web自动化领域非常成熟和流行的选择。3. 项目结构搭建与核心模块解析一个清晰的项目结构是框架的基石。它决定了代码的组织方式直接影响开发效率和维护成本。以下是我采用的目录结构python_selenium_framework/ ├── configs/ # 配置文件目录 │ ├── config.yaml # 主配置文件 │ └── elements/ # 页面元素定位器配置文件可选另一种实现方式 ├── data/ # 测试数据目录如CSV, JSON, Excel文件 ├── logs/ # 运行时日志目录 ├── reports/ # 测试报告目录如Allure报告 ├── page_objects/ # 页面对象模型目录 │ ├── __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 │ ├── driver_manager.py # 浏览器驱动管理 │ ├── logger.py # 日志记录器封装 │ └── common_actions.py # 通用操作封装 ├── requirements.txt # 项目依赖列表 └── run_tests.py # 测试运行入口脚本3.1 核心模块一浏览器驱动管理 (utils/driver_manager.py)这是框架的发动机。它的职责是创建、配置并返回一个WebDriver实例并在适当的时候关闭它。这里的关键是解决驱动下载、多浏览器支持和线程安全。实现要点驱动自动管理使用webdriver-manager库可以自动下载和匹配对应浏览器版本的驱动省去手动维护的麻烦。# requirements.txt 中添加 webdriver-manager支持多浏览器通过配置文件决定启动Chrome、Firefox或Edge。# utils/driver_manager.py from selenium import webdriver from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.firefox.service import Service as FirefoxService from selenium.webdriver.edge.service import Service as EdgeService from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.firefox import GeckoDriverManager from webdriver_manager.microsoft import EdgeChromiumDriverManager import logging class DriverManager: _instance None # 用于简单的单例模式根据需求可选 def __init__(self, browser_namechrome): self.browser_name browser_name.lower() self.driver None self.logger logging.getLogger(__name__) def create_driver(self, headlessFalse): 创建并返回一个WebDriver实例 if self.driver: return self.driver if self.browser_name chrome: options webdriver.ChromeOptions() if headless: options.add_argument(--headlessnew) # Selenium 4.11 推荐写法 options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) options.add_argument(--window-size1920,1080) # 禁用“Chrome正受到自动测试软件控制”的提示 options.add_experimental_option(excludeSwitches, [enable-automation]) options.add_experimental_option(useAutomationExtension, False) service ChromeService(ChromeDriverManager().install()) self.driver webdriver.Chrome(serviceservice, optionsoptions) elif self.browser_name firefox: options webdriver.FirefoxOptions() if headless: options.add_argument(--headless) service FirefoxService(GeckoDriverManager().install()) self.driver webdriver.Firefox(serviceservice, optionsoptions) elif self.browser_name edge: options webdriver.EdgeOptions() if headless: options.add_argument(--headless) options.add_argument(--no-sandbox) options.add_argument(--disable-dev-shm-usage) # **关键点处理Edge扩展程序提示** # 有时Edge会提示“要在Microsoft Edge中使用扩展程序吗”这会导致自动化阻塞。 # 可以通过添加特定参数来禁用这个提示。 options.add_argument(--disable-extensions) options.add_experimental_option(excludeSwitches, [enable-logging]) # 更彻底的方案使用用户数据目录并预先配置好 # options.add_argument(r--user-data-dirC:\Users\YourName\AppData\Local\Microsoft\Edge\User Data) # options.add_argument(--profile-directoryDefault) service EdgeService(EdgeChromiumDriverManager().install()) self.driver webdriver.Edge(serviceservice, optionsoptions) else: raise ValueError(fUnsupported browser: {self.browser_name}) # 设置隐式等待全局等待策略非必需建议与显式等待结合 self.driver.implicitly_wait(10) # 最大化窗口 self.driver.maximize_window() self.logger.info(f{self.browser_name.capitalize()} driver started successfully.) return self.driver def quit_driver(self): 关闭驱动并退出浏览器 if self.driver: self.driver.quit() self.driver None self.logger.info(Driver quit successfully.)注意关于Edge浏览器“启用扩展程序”的提示这是一个常见的坑。上述代码通过--disable-extensions参数来尝试禁用。但如果你的自动化流程确实需要某些扩展更稳定的做法是使用--user-data-dir加载一个已经手动登录并确认过扩展提示的浏览器用户配置文件。这需要你先用普通方式打开Edge处理完提示然后记录下该配置文件的路径。与pytest集成通常将driver的生命周期管理通过pytest的fixture来实现确保每个测试用例都有干净的上下文并在用例结束后清理资源。# test_cases/conftest.py import pytest from utils.driver_manager import DriverManager pytest.fixture(scopefunction) # 每个测试函数一个driver实例 def driver(): dm DriverManager(browser_namechrome) # 可从config读取 driver dm.create_driver(headlessFalse) # 测试时通常不无头调试方便 yield driver dm.quit_driver()3.2 核心模块二页面对象基类 (page_objects/base_page.py)POM的核心思想是将页面封装成一个类页面的元素定位器是类的属性页面的操作是类的方法。基类封装了所有页面对象共用的操作和等待逻辑。实现要点元素定位器统一管理一种做法是将定位器以字典形式定义在页面类内部另一种更灵活的做法是使用外部YAML文件管理。这里展示类内部定义的方式。显式等待封装Selenium的WebDriverWait是处理动态元素的利器。在基类中封装一个通用的等待元素可见、可点击的方法。通用操作封装如输入文本、点击元素、获取文本等可以封装在基类子类直接调用减少重复代码。# page_objects/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 import logging class BasePage: def __init__(self, driver): self.driver driver self.timeout 20 # 默认超时时间可从配置读取 self.logger logging.getLogger(__name__) def find_element(self, locator): 查找单个元素加入显式等待 try: element WebDriverWait(self.driver, self.timeout).until( EC.presence_of_element_located(locator) ) return element except TimeoutException: self.logger.error(fElement not found within {self.timeout} seconds: {locator}) # 这里可以附加截图操作方便调试 self._take_screenshot(element_not_found) raise def find_elements(self, locator): 查找多个元素 try: elements WebDriverWait(self.driver, self.timeout).until( EC.presence_of_all_elements_located(locator) ) return elements except TimeoutException: self.logger.warning(fElements not found within {self.timeout} seconds: {locator}) return [] # 返回空列表避免用例因找不到元素集合而直接失败 def click(self, locator): 点击元素等待元素可点击 element WebDriverWait(self.driver, self.timeout).until( EC.element_to_be_clickable(locator) ) element.click() self.logger.info(fClicked element: {locator}) def input_text(self, locator, text): 清空输入框并输入文本 element self.find_element(locator) element.clear() element.send_keys(text) self.logger.info(fInput text {text} into element: {locator}) def get_text(self, locator): 获取元素的文本内容 element self.find_element(locator) return element.text def _take_screenshot(self, name): 内部方法截图并保存到指定路径 screenshot_path f./logs/screenshot_{name}_{int(time.time())}.png self.driver.save_screenshot(screenshot_path) self.logger.info(fScreenshot saved to: {screenshot_path})3.3 核心模块三具体页面对象示例 (page_objects/login_page.py)基于基类创建具体的页面类。每个类对应一个网页或一个网页组件。# page_objects/login_page.py from selenium.webdriver.common.by import By from page_objects.base_page import BasePage class LoginPage(BasePage): # 页面元素定位器使用 (By.策略, 值) 的元组形式 USERNAME_INPUT (By.ID, username) PASSWORD_INPUT (By.ID, password) LOGIN_BUTTON (By.XPATH, //button[typesubmit]) ERROR_MESSAGE (By.CLASS_NAME, alert-error) def __init__(self, driver): super().__init__(driver) # 可以在这里添加页面特定的初始化逻辑比如访问登录页URL # self.driver.get(https://example.com/login) def login(self, username, password): 登录操作 self.logger.info(fAttempting to login with username: {username}) self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click(self.LOGIN_BUTTON) def get_error_message(self): 获取登录错误提示信息 try: # 错误信息可能不会立即出现需要短暂等待 return self.get_text(self.ERROR_MESSAGE) except Exception: return None # 没有错误信息可能登录成功4. 测试用例编写与pytest集成实践有了稳固的页面对象编写测试用例就变得清晰且易于维护。我们使用pytest来组织和管理这些用例。4.1 编写一个简单的登录测试# test_cases/test_login.py import pytest import logging from page_objects.login_page import LoginPage from page_objects.home_page import HomePage # 假设登录后跳转到主页 class TestLogin: 登录功能测试类 pytest.mark.smoke # 使用pytest标记可以用于筛选用例 def test_login_success(self, driver): 测试正常登录流程 logger logging.getLogger(__name__) logger.info(Starting test_login_success) login_page LoginPage(driver) # 假设我们有一个测试用的登录页 driver.get(https://example.com/login) # 执行登录操作 login_page.login(valid_user, valid_password) # 验证登录成功断言跳转到了主页并且主页有欢迎语 home_page HomePage(driver) welcome_text home_page.get_welcome_text() # 假设HomePage有这个方法 assert Welcome in welcome_text, fLogin failed, welcome text is: {welcome_text} logger.info(Login success test passed.) pytest.mark.parametrize(username, password, expected_error, [ (invalid_user, valid_password, Invalid username), (valid_user, , Password is required), (, valid_password, Username is required), ]) def test_login_failure(self, driver, username, password, expected_error): 参数化测试测试各种登录失败场景 logger logging.getLogger(__name__) logger.info(fTesting login failure with username: {username}) login_page LoginPage(driver) driver.get(https://example.com/login) login_page.login(username, password) # 验证出现了正确的错误提示 actual_error login_page.get_error_message() assert actual_error is not None, Error message should be displayed for failed login. assert expected_error in actual_error, fExpected error {expected_error} not in {actual_error} logger.info(fLogin failure test passed for username: {username})4.2 pytest配置与高级用法 (conftest.py)conftest.py是pytest的本地插件文件用于存放共享的fixture和钩子函数。# test_cases/conftest.py import pytest import logging import os from datetime import datetime from utils.driver_manager import DriverManager # 配置日志 def setup_logging(): log_dir ./logs if not os.path.exists(log_dir): os.makedirs(log_dir) log_file os.path.join(log_dir, ftest_run_{datetime.now().strftime(%Y%m%d_%H%M%S)}.log) logging.basicConfig( levellogging.INFO, format%(asctime)s - %(name)s - %(levelname)s - %(message)s, handlers[ logging.FileHandler(log_file), logging.StreamHandler() # 同时输出到控制台 ] ) # 在pytest会话开始时设置日志 def pytest_configure(config): setup_logging() # 核心fixture提供WebDriver实例 pytest.fixture(scopefunction) def driver(request): 为每个测试函数提供一个全新的浏览器驱动实例 # 可以从命令行参数或配置文件读取浏览器类型 browser request.config.getoption(--browser) or chrome headless request.config.getoption(--headless) or False dm DriverManager(browser_namebrowser) driver_instance dm.create_driver(headlessheadless) # 在测试失败时自动截图通过pytest钩子实现更优雅这里是一个简单示例 original_cls request.cls if original_cls is not None: original_cls._driver driver_instance # 将driver挂到测试类上方便其他方法访问如果需要 yield driver_instance # 测试结束后无论成功失败都退出浏览器 dm.quit_driver() # 添加自定义命令行选项 def pytest_addoption(parser): parser.addoption(--browser, actionstore, defaultchrome, helpBrowser to run tests: chrome, firefox, edge) parser.addoption(--headless, actionstore_true, defaultFalse, helpRun tests in headless mode)4.3 运行测试与生成报告运行测试可以在项目根目录下使用命令行运行测试。# 运行所有测试 pytest test_cases/ # 运行带有特定标记的测试如冒烟测试 pytest test_cases/ -m smoke # 指定浏览器运行 pytest test_cases/ --browseredge # 无头模式运行 pytest test_cases/ --headless # 运行特定文件或类 pytest test_cases/test_login.py pytest test_cases/test_login.py::TestLogin生成Allure报告首先安装Allure命令行工具需Java环境。运行测试并生成结果数据。pytest test_cases/ --alluredir./reports/allure-results生成并打开HTML报告。allure generate ./reports/allure-results -o ./reports/allure-report --clean allure open ./reports/allure-report5. 常见问题、排查技巧与进阶优化在实际搭建和运行过程中你一定会遇到各种各样的问题。这里记录了一些典型问题的排查思路和解决方案。5.1 元素定位失败自动化测试的头号敌人这是最常见的问题现象是NoSuchElementException或TimeoutException。排查清单定位器是否正确首先手动在浏览器开发者工具中使用$x()XPath或$$()CSS验证你的定位器是否能找到唯一元素。注意Selenium的XPath是严格区分大小写的。页面是否加载完成元素可能因为网络慢或JavaScript渲染而延迟出现。永远不要只依赖隐式等待。使用基类中封装的显式等待WebDriverWait来等待元素具备某种状态可见、可点击、存在等。是否在正确的iframe或window中如果元素位于iframe内必须先使用driver.switch_to.frame()切换到对应的iframe中才能定位。操作完后记得switch_to.default_content()切回来。是否有弹窗遮挡检查是否有模态框、警告框遮挡了目标元素。可能需要先处理这些弹窗。元素属性是否动态变化有些元素的id或class是每次刷新页面都会变化的。尝试使用更稳定的定位策略如通过部分文本、父级稳定元素结合相对定位XPath的//div[idstable-parent]//input或者使用CSS选择器结合属性通配符input[name^user]。页面结构是否已变更这是维护期最常见的问题。将定位器集中管理在页面对象类中一旦页面变化只需修改一处。5.2 浏览器驱动与版本兼容性问题问题WebDriverException: Message: unknown error: cannot find Chrome binary...或驱动版本不匹配。解决使用webdriver-manager如前文代码所示这是最推荐的方式它能自动处理驱动下载和版本匹配。手动指定路径如果公司网络有管制可以手动下载驱动并在代码中指定路径service ChromeService(executable_path/path/to/chromedriver)。确保浏览器已安装检查Chrome/Firefox/Edge是否已正确安装在默认路径。5.3 异步加载与复杂等待场景有时简单的presence_of_element_located不够需要等待更复杂的条件。等待元素包含特定文本from selenium.webdriver.support.expected_conditions import text_to_be_present_in_element wait WebDriverWait(driver, 10) element wait.until(text_to_be_present_in_element((By.ID, status), 完成))等待多个元素中的任何一个出现可以自定义Expected Condition。等待页面URL变化EC.url_contains(“success”)。等待JavaScript执行完成对于重度依赖JS的SPA应用可以尝试等待某个JS变量或属性。wait.until(lambda d: d.execute_script(return window.myApp.isLoaded) True)5.4 稳定性提升重试机制与截图为了提高用例在不稳定环境下的通过率可以引入重试机制。使用pytest-rerunfailures插件pip install pytest-rerunfailures# 运行命令时添加参数对失败用例重试2次每次间隔1秒 pytest test_cases/ --reruns 2 --reruns-delay 1也可以在用例上标记pytest.mark.flaky(reruns3, reruns_delay2) def test_unstable_feature(self, driver): ...失败自动截图在conftest.py中通过pytest的钩子函数在用例失败时自动截图并附加到Allure报告或保存到本地。# conftest.py (补充) import allure pytest.hookimpl(tryfirstTrue, hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield rep outcome.get_result() if rep.when call and rep.failed: # 获取driver fixture driver_fixture item.funcargs.get(driver) if driver_fixture: # 截图并附加到Allure报告 allure.attach(driver_fixture.get_screenshot_as_png(), namescreenshot_on_failure, attachment_typeallure.attachment_type.PNG) # 或者保存到本地文件 screenshot_path f./logs/failure_{item.name}_{datetime.now().strftime(%H%M%S)}.png driver_fixture.save_screenshot(screenshot_path) print(fScreenshot saved to: {screenshot_path})5.5 框架的进一步扩展方向当基础框架搭建完毕后可以考虑以下方向进行扩展使其更强大数据驱动测试使用pytest的pytest.mark.parametrize装饰器或者结合pandas读取Excel/CSV文件将测试数据与测试逻辑分离。API与UI混合测试集成requests库在UI测试前先通过API准备测试数据如创建用户测试后通过API清理数据使测试更独立、更快速。数据库验证集成pymysql或sqlalchemy在UI操作后直接查询数据库验证数据是否正确落库。邮件/通知服务测试完成后通过smtplib或集成企业微信/钉钉机器人将测试结果发送给相关人员。与CI/CD集成编写Jenkinsfile或GitLab CI的.gitlab-ci.yml配置文件将自动化测试集成到持续交付流水线中实现代码提交后自动触发回归测试。容器化运行使用Docker将测试环境包括浏览器、驱动、Python环境容器化确保测试在任何机器上运行结果一致也便于在CI服务器上部署。构建一个Web自动化框架是一个迭代的过程。不要试图一开始就设计一个完美无缺的庞然大物。从最核心的页面对象模型和驱动管理开始编写一两个实际的测试用例在过程中发现问题、解决问题、逐步重构和添加新功能。这个笔记提供的正是这样一个可以立即起步的“最小可行框架”希望它能成为你探索Web自动化世界的一块坚实跳板。记住框架的价值在于服务于高效的脚本编写和稳定的测试执行而不是框架本身有多复杂。