资讯详情

从零搭建Python+Selenium自动化测试框架:从脚本到可维护体系的完整实践

📅 2026/10/10 23:17:10 | 华诺云谱 👁 阅读
从零搭建Python+Selenium自动化测试框架:从脚本到可维护体系的完整实践
1. 先想清楚脚本和框架的差距到底在哪里我面试过不少投自动化测试岗位的人简历上都写着“熟悉Selenium、能编写自动化脚本”可真正上手搭框架时很多人会沉默。问题不是不会写driver.find_element().click()而是只会这一句。能把用例跑通和能搭建一套可维护、可扩展的自动化测试框架完全是两个层面的能力。先说结论脚本解决的是“某一条用例能不能跑通”框架解决的是“一百条用例能不能稳定、有序、低成本地跑完并且出了问题能快速定位”。如果你只是临时写一段代码验证某个流程那确实不需要框架写个脚本就够了。但一旦进入项目级回归、多轮迭代、多人协作代码就会失控用例之间有依赖、定位方式五花八门、浏览器Driver版本对不上、执行完了没有报告、报错了不知道在哪一步。这篇文章要聊的就是怎么从零搭一套“能落地”的PythonSelenium自动化测试框架。我理解的“能落地”包含下面这几个硬指标环境变化时驱动和浏览器版本不拖后腿元素定位统一封装页面改动时修改成本可控用例逻辑与测试数据分离加一条用例的代价足够小跑完之后有清晰的报告失败时有截图和日志可以接入CI没人值守也能稳定执行。这套框架适合正在做Web端测试、准备从零搭建自动化体系的测试工程师也适合已经写了大量脚本、但感觉维护成本越来越高的朋友去对照整理自己的代码。下面所有内容都来自我实际搭建和维护过的项目我把其中踩过的坑和选型思路也一并写出来。2. 环境搭建的隐蔽坑Python、编辑器、浏览器驱动三件套很多新手在第一步就被卡住了卡的还不是写代码而是环境。这里我把几个高频坑单独拎出来讲清楚。2.1 安装Python与VSCode配置Python安装本身不难但容易在两个地方翻车第一勾选项。安装时底部有个“Add Python to PATH”的选项默认不勾选很多人直接下一步跳过。装完之后在命令行敲python提示找不到命令。这个选项一定要勾上否则后续还得手动配置环境变量。如果已经装完了去“系统属性-环境变量-Path”里手动加上Python安装目录和Scripts子目录。第二多版本冲突。公司电脑里可能已经有一个Python版本还不一定匹配。我建议装个pyenv或者直接用python3命令区分别轻易动系统自带的解释器。Windows用户如果没有多版本需求直接官网下载最新稳定版就行。装完之后VSCode里装好Python插件按CtrlShiftP打开命令面板选择解释器指向刚装的那个Python。这个步骤不做VSCode会用系统默认解释器装第三方库时也会装错地方。2.2 webdriver-manager让Driver版本问题彻底消失这是整个环境搭建里最值得提前做的一件事。以前我们手动下载ChromeDriver把exe丢到项目里然后代码里写死路径。Chrome浏览器一升级驱动版本就对不上报SessionNotCreatedException。整个团队人仰马翻。后来我改用webdriver-manager这个库Driver问题基本消失了。from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager def create_driver(): service Service(ChromeDriverManager().install()) options webdriver.ChromeOptions() return webdriver.Chrome(serviceservice, optionsoptions)webdriver-manager会根据当前浏览器的版本号自动下载匹配的驱动并缓存到本地。以后浏览器升级、驱动不对齐的问题框架层面直接消化掉了。另外提醒一句如果你用Chrome版本比较新无头模式参数有变化。Chrome 111之后的版本建议用--headlessnew而不是旧的--headless否则部分功能在无头模式下表现异常。2.3 第一个能跑的冒烟用例先别谈分层先跑通环境搞定后先写一个最小用例验证Selenium本身没问题。from selenium import webdriver from selenium.webdriver.common.by import By from time import sleep driver webdriver.Chrome() driver.get(https://www.baidu.com) driver.find_element(By.ID, kw).send_keys(selenium) driver.find_element(By.ID, su).click() sleep(3) assert selenium in driver.title driver.quit()这个用例能跑通说明Python、Selenium、驱动、浏览器四者链路是通的。后面所有框架设计都建立在这个基础上。跑不通也没关系按错误类型排查ModuleNotFoundError是库没装对SessionNotCreatedException是驱动版本不对WebDriverException多半是浏览器进程残留或端口被杀。3. 框架的灵魂Driver管理、BasePage与统一等待机制这一节是整个框架最核心的部分。很多半吊子框架之所以烂尾就是因为这三个东西没有设计好。3.1 单例Driver工厂别让每个测试类都new一次浏览器如果你的代码长这样每个测试类里都webdriver.Chrome()每跑一个用例就开一个浏览器跑完就关那我有两个建议请立刻统计一下你整个回归套件跑完要多少分钟然后把这个数字除以用例数看看平均每条用例的成本请去查一下“单例模式”这个词然后把Driver改成全局唯一。我用的方案是一个Driver工厂类内部持有唯一实例from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager class DriverFactory: _driver None def __init__(self, headlessFalse): self.headless headless classmethod def get_driver(cls): if cls._driver is None: cls._driver cls._create_driver() return cls._driver classmethod def _create_driver(cls): service Service(ChromeDriverManager().install()) options webdriver.ChromeOptions() options.add_argument(--start-maximized) if cls.headless: options.add_argument(--headlessnew) options.add_argument(--no-sandbox) options.add_argument(--disable-gpu) return webdriver.Chrome(serviceservice, optionsoptions) classmethod def quit_driver(cls): if cls._driver: cls._driver.quit() cls._driver None这样做的理由是浏览器启动的耗时在Web自动化的总耗时里占比极高。单例模式下整个测试会话只启动一次浏览器用例之间复用同一个会话。虽然每一条用例的执行速度提不上来但回归套件的总时长至少能缩减30%-50%。3.2 BasePage封装把定位、点击、输入这些动作沉淀成公共方法BasePage是Page Object模式的基石也是新手最容易误解的地方。有人把页面元素和业务流程全塞进测试用例里导致用例步骤又长又乱。我做BasePage的核心原则只有一个把Selenium最常用、最容易被写错的动作统一封装成带等待、带日志、带异常处理的方法。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By from selenium.common.exceptions import TimeoutException class BasePage: def __init__(self, driver, timeout10): self.driver driver self.wait WebDriverWait(driver, timeout) def click(self, locator, timeout10): element WebDriverWait(self.driver, timeout).until( EC.element_to_be_clickable(locator) ) element.click() def input_text(self, locator, text, timeout10): element WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator) ) element.clear() element.send_keys(text) def get_text(self, locator, timeout10): element WebDriverWait(self.driver, timeout).until( EC.visibility_of_element_located(locator) ) return element.text def find_element(self, locator, timeout10): return WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) )这里有个细节值得强调locator统一用元组比如(By.ID, username)不要用字符串写死。所有的定位方式都通过元组传递切换定位方式时只改页面对象里的定义测试用例的代码一句都不用动。3.3 显式等待为什么要全面替代隐式等待这是前端自动化里最老生常谈但实际项目里错误率最高的问题之一。隐式等待用driver.implicitly_wait(10)设置的是一次性的对话级等待它对所有find_element调用生效。听起来很方便但它有一个致命弱点它只等待元素出现在DOM中不判断元素是否可点击、是否可见。现代前端大量采用异步渲染DOM里有了元素不代表你能点它、能操作它。更麻烦的是隐式等待和显式等待混用时两者的超时时间会叠加排查定位超时问题时会非常头痛。我建议项目中彻底禁止隐式等待所有主动操作都走显式等待。显式等待的逻辑很简单明确告诉Selenium你等什么、等到什么条件为止。element_to_be_clickable、visibility_of_element_located、presence_of_element_located是三个最常用的条件分别对应可点击、可见、存在于DOM三种状态。选型上能等可点击就不要等存在能等可见就不要等存在——越贴近用户实际感知的状态脚本越稳定。4. pytest整合用例组织、数据驱动与报告生成Selenium本身没有测试框架的能力它只负责“操作浏览器”。用例编排、断言、收集、报告这些活需要pytest来做。pytest和Selenium的配合是整个项目里最顺滑的部分。4.1 conftest和fixture浏览器会话的生命周期管理pytest的fixture机制给了我们非常优雅的Driver生命周期管理方式。在项目根目录放一个conftest.py里面定义fixturepytest会自动加载它conftest里的fixture对所有测试模块生效import pytest from core.driver_factory import DriverFactory pytest.fixture(scopefunction) def driver(): factory DriverFactory(headlessFalse) driver factory.get_driver() yield driver factory.quit_driver()这里scopefunction的含义是每条用例执行前都不会复用浏览器会话保证用例间隔离。如果你希望整个测试类共用一个浏览器可以改成scopeclass。三种scope的使用原则function每条用例独立浏览器隔离性最好速度最慢class同一测试类共享浏览器速度快一些但用例之间可能互相影响session整个测试会话共享一个浏览器速度最快但用例耦合风险极高。我做回归测试时一般用function做冒烟验证时用class就够了。不要一上来就追session级速度等到用例稳定性足够高了再提。4.2 pytest.ini不要用一堆命令行参数来跑测试很多团队跑测试是手工敲参数的pytest -v -s --htmlreport/report.html test_login.py。这有个问题项目换个人接手参数怎么敲的都不知道。我更建议把公共配置统一写进pytest.ini[pytest] addopts -v -s --htmlreport/report.html --self-contained-html testpaths testcases python_files test_*.py python_classes Test* python_functions test_*testpaths指定用例目录python_files、python_classes、python_functions定义了pytest收集用例的命名规则。这样任何人只要在终端敲一个pytest所有公共参数都会生效。新成员加入项目时不需要背一堆命令参数出错的概率直线下降。4.3 数据驱动让一条用例可以跑十组数据最原始的写法是把测试数据写死在用例里def test_login(driver): page LoginPage(driver) page.login(admin, 123456) assert page.check_success() 登录成功数据一变代码就必须改。测试组数一多复制粘贴的用例代码会膨胀到不可维护。我习惯用YAML文件pytest的parametrize实现数据驱动先建一个test_data/login_data.yamllogin_valid_cases: - username: admin password: 123456 expected: 登录成功 - username: user01 password: 123456 expected: 登录成功 - username: admin password: wrong_password expected: 密码错误然后在测试代码里读取import pytest import yaml from pages.login_page import LoginPage with open(test_data/login_data.yaml, encodingutf-8) as f: login_data yaml.safe_load(f) pytest.mark.parametrize(case, login_data[login_valid_cases]) def test_login(driver, case): page LoginPage(driver) page.login(case[username], case[password]) assert page.get_login_message() case[expected]加一条数据就是往YAML里加一行测试逻辑一行不动。这就是数据驱动最直观的价值测试数据和测试逻辑彻底解耦谁都能维护数据文件就算不会写代码的人也能往里面加测试用例。这里有个关联热词“pytest自动化测试框架”值得展开说pytest的参数化机制正确的使用方式是让“测试逻辑”和“测试数据”分家。很多人把参数化写成了简单的for循环结果每条用例的数据错了都不知道是哪一组挂的。用parametrize之后pytest会自动把每组数据生成为独立的测试用例失败时能精确到是哪一组数据引发的排查问题效率完全不同。5. 真实业务里必须处理的几个高频问题框架搭好了用例写顺了不等于万事大吉。下面这几个问题是我在真实项目里反复遇见的也是网络热词里被大量搜索的高频痛点。5.1 iframe与多窗口切换一半的定位失败都源于此前端页面里嵌iframe是常见操作Selenium默认情况下只能访问顶层DOM看到iframe里的元素会直接报NoSuchElementException。处理套路其实很固定from selenium.webdriver.common.by import By # 等待iframe加载并切换进去 wait.until(EC.frame_to_be_available_and_switch_to_it((By.ID, iframe_id))) # 操作iframe内部的元素 driver.find_element(By.ID, element_inside_iframe).click() # 操作完记得切回主文档 driver.switch_to.default_content()多窗口的情况更隐蔽。点击一个链接浏览器开了新标签页driver还在旧页面上。此时要用窗口句柄切换# 点击前先记录当前窗口句柄 main_handle driver.current_window_handle # 点击触发新窗口 driver.find_element(By.ID, open_new_window).click() # 等待新窗口出现 wait.until(EC.number_of_windows_to_be(2)) # 遍历找到新窗口句柄并切换 for handle in driver.window_handles: if handle ! main_handle: driver.switch_to.window(handle) break这里有个细节每次切换完窗口等元素之前要确认新页面的加载状态否则直接找元素很容易撞上“慢加载”的空窗期。5.2 页面滚动与懒加载元素看似存在实则点不了热词里“selenium 网页左右滑动”“selenium 左右滚动可见”两条指向的就是这个问题。很多Web页面是懒加载设计元素已经渲染在HTML里了但不在可视区点起来大概率失败。我处理滚动问题有两种方案按场景选择第一种滚动到元素可见再点击。适用于元素存在于DOM但不在可视区的情况def scroll_to_element_and_click(self, locator, timeout10): element WebDriverWait(self.driver, timeout).until( EC.presence_of_element_located(locator) ) self.driver.execute_script(arguments[0].scrollIntoView({block:center});, element) WebDriverWait(self.driver, timeout).until(EC.element_to_be_clickable(locator)).click()block:center的意思是让元素滚动到视口中间位置比scrollIntoView()默认参数更稳底部悬浮组件遮住元素这类问题也能规避一部分。第二种纵向或横向滚动固定增量。用于瀑布流、长列表页面需要持续滚动页面直到元素出现def scroll_page_until_found(self, locator, max_scroll_times10, scroll_step800): for _ in range(max_scroll_times): try: element self.driver.find_element(*locator) self.driver.execute_script(arguments[0].scrollIntoView({block:center});, element) return element except: self.driver.execute_script(fwindow.scrollBy(0, {scroll_step});) sleep(0.5) raise TimeoutException(f滚动{max_scroll_times}次后仍未找到元素: {locator})注意循环里必须加sleep或隐式等待否则页面还没渲染下一批数据就白滚了几次。这个细节很多教程不会讲实际跑的时候才会发现“滚动失效”、“找不到元素”往往不是滚动方法错了而是页面渲染速度跟不上脚本滚动速度。5.3 失败截图与错误日志让报错信息自己说话用例挂掉的时候如果只有一份堆栈信息排查起来效率极低。我强烈建议在pytest的conftest.py里加一个失败自动截图机制import pytest from datetime import datetime pytest.hookimpl(hookwrapperTrue) def pytest_runtest_makereport(item, call): outcome yield report outcome.get_result() if report.when call and report.failed: driver item.funcargs.get(driver) if driver: timestamp datetime.now().strftime(%Y%m%d_%H%M%S) driver.save_screenshot(freport/screenshots/{item.name}_{timestamp}.png)这段逻辑用HookWrapper截获pytest的测试报告事件用例失败时自动取当前的driver把当时的页面截图保存下来。看到报告时截图、错误类型、堆栈信息三者对照大部分问题几分钟内就能定位。6. 框架跑起来之后的事稳定性维护与CI集成框架的搭建只是前奏真正考验功力的是后续的稳定性和集成。6.1 用例稳定性先看“玄学失败”再看代码问题用例跑起来之后你大概率会遇到“上次跑过这次就挂”“本地能过环境上就挂”的所谓玄学问题。我的经验是所谓玄学失败基本都是等待策略没设计好。举个例子用户注册后跳转首页首页有个欢迎消息。如果直接用element_to_be_clickable去等待欢迎消息关闭按钮平时很快但后端偶发延迟时脚本会在等待超时后直接报错。正确处理方式是给这个场景搭配一个显式等待的兜底def wait_for_text(self, locator, text, timeout15): return WebDriverWait(self.driver, timeout).until( EC.text_to_be_present_in_element(locator, text) )写用例时等待条件尽量精确到业务状态——等“按钮变为可点击”而不是等“页面加载完”等“特定文本出现在页面上”而不是等“某个元素存在”。这个思路转换过来之后用例的稳定性会有质的提升。6.2 集成到CI无人值守的核心配置框架维护好之后接入CI其实是顺水推舟的事。这里给一套最简配置可以直接抄构建机上装好Chrome浏览器和Python环境用pip install -r requirements.txt装依赖其中必须有webdriver-manager让Driver自动匹配浏览器版本跑测试用Headless模式DriverFactory里初始化时传headlessTrue测试命令pytest --htmlreport/report.html --self-contained-html有一个环境问题必须提前处理CI构建机通常用root或系统账户跑任务Chrome在root权限下默认会报--no-sandbox相关错误。我们的驱动选项里已经加了--no-sandbox和--disable-gpu这个配置务必保留否则CI环境一换就直接翻车。另外一个容易被忽略的点是屏幕分辨率。Headless模式下默认窗口尺寸很小很多响应式页面在窄屏下布局不同元素位置和可见性都会变化。建议在options里显式设置窗口大小options.add_argument(--window-size1920,1080)这样CI环境和本地视觉表现一致避免“本地过了CI挂了”的奇葩问题。6.3 框架持续演进从“能用”到“好用”框架落地之后保持小步快跑的维护节奏更重要。我的建议是每两周留出半天时间做一次框架层整理重点看这几个方面定位是否更稳定页面结构变了定位方式是否需要从class name换成data-testid或xpath等待条件是否可以更精确有没有应该用text_to_be_present_in_element却用了visibility_of_element_located的地方用例间是否有隐性依赖有没有用例A必须依赖用例B先执行这种潜在的先后顺序迟早会出问题应该靠fixture隔离而不是靠执行顺序保障。这些都是框架上线三个月后才会冒出来的问题最早搭框架的时候你可能感知不到但运行一段时间回头看优化空间一定比想象中大。我在实际项目里维护这套框架三年最大的体会是自动化测试框架很难一次搭好它一定是在真实业务用例的摩擦中一点点长起来的。不要追求一开始就大而全先把Driver管理、BasePage、pytest整合这三件事做扎实后面的一切都会顺畅很多。最后分享一个小技巧框架里所有核心工具的版本号记得统一放在requirements.txt里固定下来新同事入职时只需要跑一条pip install -r requirements.txt环境问题能减少一半以上。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑