简介:这是一份基于Python+Selenium的web自动化测试框架设计与实现方向的毕业设计文档,适合软件测试初学者、自动化测试工程师以及正在搭建Selenium框架的开发者学习参考。内容从传统手工测试容易产生疲劳和测试盲点的弊端切入,系统讲解软件测试理论基础、自动化测试分类、Web应用测试内容与框架需求分析,并结合具体实现说明如何利用Selenium完成UI布局、兼容性、稳定性等自动化测试,让读者能把握自动化测试生命周期、高质量测试过程建立等关键环节。资源为doc格式,压缩包包含1个文件,整体大小约1.53MB,下载后可直接阅读与打印。目前已有497人学习下载,对于测试方向学习者有不错的参考价值。文档内含中英文摘要、目录及完整章节,深入阐述自动化测试框架的分类、设计与实现过程,还涉及人工智能、机器学习等未来发展方向的探讨,既可用于相关论文写作、课程设计参考,也可作为实际搭建测试框架时的设计蓝本。
1. 基于 Python+Selenium 的 web 自动化测试框架:从脚本到能被团队接管的工程
不管你是刚接触 Selenium 的测试新人,还是已经写了几年“能跑就行”的自动化脚本,最后大概率都会撞上同一个问题:脚本越写越多,但维护它们的时间已经快超过手工回归的时间了。元素定位写得到处都是,前端一改版,几十个用例集体报错,根本不知道是哪一层出了问题,也没人敢拍胸脯说这套东西能在他自己的电脑上跑起来。这篇文章要聊的,就是怎么把散落的 Selenium 脚本,整理成一个有分层、有公共封装、有配置管理、有报告输出的自动化测试框架——也就是标题里那句“设计与实现”真正在讲的事。
框架的本质不是引入多少新工具,而是定规矩:用例该怎么写、页面对象怎么放、浏览器驱动怎么管、失败截图存到哪、报告怎么生成。Python 负责粘合,Selenium 负责驱动浏览器,两者的分工本身就决定了框架的结构。下面就从最核心的分层讲起,再落到能直接抄走的工程搭建和实战用例,最后用两个高频痛点——登录态复用和失败重试——收尾。整个过程中参数怎么调、坑在哪,都会给到具体做法。
2. 设计框架前先理解分层:为什么裸写 Selenium 跑不长
2.1 脚本堆叠式的自动化为什么活不过三个月
直接写 Selenium 脚本跑用例,入门门槛很低:webdriver.Chrome()、find_element(By.ID, "username")、send_keys(),几条 API 一拼,一个登录用例就通了。但这样的代码一旦超过十个用例,问题就开始成片出现。最常见的情况是:登录逻辑被复制到了每个用例里,一个登录框的定位变化,所有用例都要改;测试数据写死在脚本里,换一套环境就得全文搜索替换;浏览器配置、等待策略、失败截图散落在各处,根本没有统一的出口。
所以一个能被长期维护的 web 自动化测试框架,第一原则就是分层——让每一类职责待在一个明确的目录里,不让用例层的人去关心浏览器是怎么启动的,也不让页面对象层的人去关心报告文件名的格式。Selenium 官方文档一直没有强制给出“你应该这样组织代码”,但社区在大量实战后沉淀出了一个公认的最简分层结构。
2.2 四层结构:用例层、业务层、页面对象层、公共层
实际落地时,我一般会把框架切成下面四层,每一层的职责边界要非常清楚:
| 层级 | 目录/模块 | 职责 | 典型内容 |
|---|---|---|---|
| 用例层 | testcases/ | 业务场景的组装,只做“安排”,不做执行细节 | test_login.py, test_order.py |
| 业务层 | business/ | 跨页面的业务流程,比如下单要经过搜索页、详情页、购物车 | 订单流程、支付流程 |
| 页面对象层 | pages/ | 每一个页面封装成一个类,元素定位和数据操作都在类里 | LoginPage, SearchPage |
| 公共层 | common/ | 与业务无关的通用能力,所有层都能调用 | browser.py, config.py, logger.py, report.py |
页对象层尤其是框架的命根子。一个页面写一个类,类里面只做两件事:描述这个页面上有哪些元素、这些元素能做什么操作。比如登录页的LoginPage,里面既不应该有“断言登录成功”的逻辑,更不应该有生成测试报告的逻辑,它的职责是暴露input_username()、input_password()、click_login()这些方法,让用例层去组合。前端改版时,最坏情况下只需要改对应页对象的内部定位,用例层的代码一行都不用动。
这样一来,框架的每一层都可以独立演化:公共层升级了浏览器启动方式,页面对象层和用例层完全无感知;用例层新增了一条测试场景,公共层和页面对象层也完全不用动。这就是分层的价值——把变化隔离在最小范围内。
2.3 热加载配置:环境切换不需要改代码
框架的配置管理一定要做的第一件事,就是把环境相关的信息从代码里剥离出来。URL、账号、超时时间、浏览器类型、是否无头模式,这些都是环境配置,不是业务代码。常见的做法是在工程根目录放一个 YAML 或者 JSON 配置文件,公共层写一个ConfigManager来读取。我一般会推荐 YAML,因为它的注释能力比 JSON 好,字典嵌套的写法也更直观。
配置文件大约长这样:
# config/config.yaml env: test base_url: https://demo.example.com browser: type: chrome # chrome / firefox / edge headless: false # 服务器上跑建议 true implicit_wait: 10 # 隐式等待秒数 page_load_timeout: 30 user: username: tester01 password: "P@ssw0rd" report: path: reports/ title: "Web Auto Test Report"公共层里的配置读取模块,核心逻辑就是把这个 YAML 解析成 Python 字典,然后供全工程调用:
import yaml from pathlib import Path class ConfigManager: """全局配置管理:启动时读取一次 YAML,后续通过实例属性访问""" def __init__(self, config_path: str = "config/config.yaml"): self._config = self._load(config_path) def _load(self, config_path: str) -> dict: path = Path(config_path) if not path.exists(): raise FileNotFoundError(f"配置文件不存在: {path.resolve()}") with open(path, encoding="utf-8") as f: return yaml.safe_load(f) def get(self, key: str, default=None): # 支持点号路径,例如 browser.headless node = self._config for part in key.split("."): if not isinstance(node, dict) or part not in node: return default node = node[part] return node config = ConfigManager()这里的要点是get()方法支持了browser.headless这种点号路径访问,省去了在业务代码里写多层的["browser"]["headless"]。这个设计对嵌套深的配置结构非常关键——你的配置项一旦多起来,每次取值都手写路径,不仅啰嗦,而且容易在None上报错。
配置层还有一种更进阶的用法:用环境变量覆盖 YAML 里的值。比如在 Jenkins 上跑测试时,测试环境地址肯定和本机不一样,如果在 YAML 里写死,就得在 CI 机器上再维护一份配置文件。常见做法是,在ConfigManager的get()方法里增加一个逻辑:如果存在同名环境变量,就用环境变量的值覆盖 YAML 里的值,这能让框架适配不同执行环境。
3. 搭建工程骨架:浏览器驱动、目录结构和第一个能跑的用例
3.1 环境安装:webdriver-manager 是最省心的选择
很多初学者在搭建 Selenium 环境时,卡在浏览器驱动的下载上。Chrome 升级后,chromedriver 版本不匹配,脚本直接报SessionNotCreatedException。你的本机还能手动下载,到了 CI 服务器上就麻烦了。2024 年以后的 Selenium 版本,实际上官方已经推荐用webdriver-manager这个库来处理驱动版本匹配的问题,它在 Selenium Manager 功能正式集成之前一直是社区事实标准。
安装依赖很简单,建议使用虚拟环境隔离:
python -m venv .venv source .venv/bin/activate pip install selenium webdriver-manager pytest pytest-ordering安装过程如果网速不行,可以换用国内镜像源。装完后,最简单的浏览器启动代码是这样的:
# common/browser.py from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager def create_driver(headless: bool = False): """创建 Chrome 浏览器实例,自动匹配本机 Chrome 版本对应的驱动""" service = Service(ChromeDriverManager().install()) options = webdriver.ChromeOptions() if headless: options.add_argument("--headless=new") options.add_argument("--window-size=1920,1080") options.add_argument("--disable-gpu") options.add_argument("--no-sandbox") return webdriver.Chrome(service=service, options=options)这里的ChromeDriverManager().install()会自动检测你本机安装的 Chrome 版本号,下载匹配的 chromedriver 并缓存在用户目录。如果本机还没装 Chrome,它会直接报错提示——这个依赖关系要清楚,webdriver-manager 管的是驱动,不管浏览器本体。生产环境里 CI 机器建议预装固定版本的 Chrome,并且锁定service = Service(ChromeDriverManager(driver_version="114.0.5735.90").install()),不要每次构建都去检查最新版,否则版本漂移会让你某天早上突然收到一批失败用例。
3.2 目录结构:一张图看懂整个工程
工程搭出来后的目录结构如下,每个目录的职责要跟团队说清楚,不然很快就有人乱放了:
web_auto_framework/ ├── config/ │ └── config.yaml # 环境配置、浏览器参数、账号信息 ├── common/ │ ├── __init__.py │ ├── browser.py # 浏览器驱动创建与销毁 │ ├── config.py # 配置读取 │ ├── logger.py # 日志封装 │ └── report.py # HTML 报告生成 ├── pages/ │ ├── __init__.py │ ├── base_page.py # 页面对象基类,封装显式等待 │ ├── login_page.py │ └── home_page.py ├── testcases/ │ ├── __init__.py │ ├── conftest.py # pytest fixtures │ └── test_login.py ├── reports/ └── requirements.txtbase_page.py 是整个页面对象层的地基。Selenium 原生的find_element不带等待,页面加载慢一点就直接NoSuchElementException。所以我在基类里统一封装了显式等待的逻辑,这样所有页面对象都共用一套等待策略,等待超时时报错信息也统一格式。
# pages/base_page.py from selenium.webdriver.remote.webdriver import WebDriver from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC class BasePage: """所有页面对象的基类:统一管理元素等待和基础操作""" def __init__(self, driver: WebDriver, timeout: int = 10): self.driver = driver self.timeout = timeout def find_element(self, locator): """显式等待元素出现后返回元素对象;超时抛出带定位信息的异常""" element = WebDriverWait(self.driver, self.timeout).until( EC.presence_of_element_located(locator) ) return element def click(self, locator): """等待元素可点击后点击""" element = WebDriverWait(self.driver, self.timeout).until( EC.element_to_be_clickable(locator) ) element.click() def input_text(self, locator, text: str): """清空输入框并输入文字""" element = self.find_element(locator) element.clear() element.send_keys(text)这里有个细节值得单独说:find_element里的locator是一个元组,比如(By.ID, "username")。把这个元组作为参数在各层之间传递,是 Selenium 比较规范的做法——不要用“字符串拼接表达式”的方式去构造定位器,那会让代码完全不可维护。element_to_be_clickable比presence_of_element_located更严格,它不但要求元素在 DOM 里,还要求可见并且可用,所以用在 click 操作上更合适。input_text里先 clear 再 send_keys 是必要的,不要假设输入框初始是空的。
3.3 启动浏览器只能用一次:session 级 fixture 的关键作用
pytest 的 fixture 机制是连接测试代码和框架的桥梁。我之前看到很多人把浏览器启动写在setup_method里,每个用例都是全新的浏览器,一个用例场景如果有 5 步操作,每步都重新开一个浏览器,光启动时间的开销就能让一套用例的耗时长十几倍。正确的做法是把 driver 定义成 session 级别的 fixture,整个测试周期内只有一个浏览器实例,所有用例共享,用例之间通过操作步骤去跳转页面。
下面是一个能直接抄进 conftest.py 的版本:
# testcases/conftest.py import pytest from common.browser import create_driver from common.config import config @pytest.fixture(scope="session") def driver(): """整个测试会话共用一个浏览器实例;结束后自动关闭""" _driver = create_driver(headless=config.get("browser.headless", False)) _driver.maximize_window() yield _driver _driver.quit() @pytest.fixture(scope="session") def base_url(): """读取测试环境根地址""" return config.get("base_url") @pytest.fixture(scope="session") def login_info(): """读取测试账号信息""" return { "username": config.get("user.username"), "password": config.get("user.password"), }看到这里你可能会有疑问:session 级共享一个浏览器,那多个用例的执行顺序不是互相影响吗?比如登录用例跑完了,会话还在,下一个用例是否不需要再登录了?这个问题的答案需要想清楚:如果下一个用例恰好是一个需要登录态的操作,那它应该依赖“前置条件已满足”。所以用例层要谨慎使用 session 共享的浏览器,通常在同一个模块下的用例按顺序执行问题不大,但跨模块的场景,最好让每个用例的关键前置步骤保持完整。
conftest.py里还有一个高频用法是失败截图。用例失败时自动截图、自动附加到日志,这在排查问题的时候帮助特别大。这个能力可以通过 pytest 的pytest_runtest_makereport钩子来实现,等框架能跑通之后随时可以加上。
3.4 第一个用例跑通:不要急着写业务,先验证框架
框架搭好的第一个验证用例,不要直接写登录、写下单,先写一个人人都会的操作:打开百度首页,断言标题。这个用例的意义不是测百度,而是验证浏览器驱动、配置读取、webdriver-manager 三者连通没问题,所有环境变量在目标机器上都是通的。
# testcases/test_smoke.py import pytest def test_open_baidu(driver, base_url): driver.get("https://www.baidu.com") assert "百度" in driver.title print(f"当前页面标题: {driver.title}")在这个用例里,driver直接来自 conftest.py 的 fixture,base_url参数没用到,但传进来了,方便后续扩展。如果这个用例在你的机器上 30 秒内跑通,说明框架最核心的部分已经成功。如果create_driver卡住或者报错,优先检查 Chrome 版本和 chromedriver 的匹配问题——用chrome://version/查看版本号,再用 webdriver-manager 手动触发一次下载,确认驱动版本没选错。这一步排查完,剩下的工作就是往框架里填页面对象和用例。
4. 实战:用页面对象模型写登录用例,并从零搭一个测试页面
4.1 没有测试环境怎么办:用 Mock 服务解决依赖问题
实际负责过自动化项目的人肯定有这种体验:被测系统的测试环境不稳定,今天能打开,明天 502,更多时候是根本不知道环境地址在哪。网上很多教程用 GitHub 上的开源项目做演示,那你就要考虑网络和教育网的兼容性,很多网络环境下那些网站根本打不开。
用 Flask 自己搭一个最小可用的测试页面是一个稳妥的选择。它能让你完全掌控页面元素,而且不管在什么网络环境下都能跑通。不要觉得 mock 出来的页面不真实,实际上 mock 页面在接口联调和自动化预演阶段的价值很大,而且你用 mock 搭出来的登录页,结构和真实系统登录页几乎是一样的。
以下是一个供 Selenium 操作的最小登录页面服务端代码。保存为mock_app.py:
# mock_app.py from flask import Flask, request, redirect, session, render_template_string app = Flask(__name__) app.secret_key = "test-secret" # 页面模板:一个最简登录表单 HTML = """ <!DOCTYPE html> <html> <head><title>Mock Login</title></head> <body> <form method="post" action="/login"> <input type="text" id="username" name="username" placeholder="用户名" /> <input type="password" id="password" name="password" placeholder="密码" /> <button type="submit" id="login-btn">登录</button> </form> <div id="msg" style="display:none"></div> </body> </html> """ @app.route("/", methods=["GET"]) def index(): if session.get("logged_in"): return "已登录" return render_template_string(HTML) @app.route("/login", methods=["POST"]) def login(): username = request.form.get("username") password = request.form.get("password") if username == "admin" and password == "admin123": session["logged_in"] = True return redirect("/") return "用户名或密码错误", 401 if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=False)启动 mock 服务:python mock_app.py,它就在本机 5000 端口提供登录页面。这个 mock 服务要包含两个路径:根路径返回登录表单,/login路径负责校验账号。为了更贴近真实系统,username和password两个字段名要和常见系统保持一致。
4.2 LoginPage 与 HomePage:页面对象层怎么落地
有了 mock 服务,就可以写真正的页面对象了。登录页面LoginPage继承自BasePage,对外暴露的是语义化操作,内部藏着具体的定位器。业务代码只关心输入账号、输入密码、点登录,不关心这些元素是什么 id 什么 name,这是页面对象模型的核心价值。
# pages/login_page.py from selenium.webdriver.common.by import By from pages.base_page import BasePage class LoginPage(BasePage): """登录页面对象:封装登录相关的所有操作""" # 定位器集中在类属性,改版时只改这里 USERNAME_INPUT = (By.ID, "username") PASSWORD_INPUT = (By.ID, "password") LOGIN_BUTTON = (By.ID, "login-btn") def input_username(self, username: str): self.input_text(self.USERNAME_INPUT, username) def input_password(self, password: str): self.input_text(self.PASSWORD_INPUT, password) def click_login(self): self.click(self.LOGIN_BUTTON) def login(self, username: str, password: str): """组合操作:一步完成登录流程""" self.input_username(username) self.input_password(password) self.click_login()定位器集中放在类属性顶部,是为了让页面元素变更的影响面最小化。在实际项目中,建议定位器不要散在方法里,而是统一集中在页面类前面,这样看到页面类就能知道这个页面有哪些关键元素。login()方法把三步操作组合在一起,用例层可以直接调用,不需要每个用例都重复写三次调用的代码。组合操作在框架里叫业务步骤,可以根据场景自由组合。
4.3 在 pytest 里写登录用例,并使用参数化把异常场景覆盖上
页面对象写好后,登录用例就变成简单的组装工作。pytest 的参数化能力在这里很实用,一批账号密码组合可以生成多个用例,不必为每个数据单独写一条代码。
# testcases/test_login.py import pytest from pages.login_page import LoginPage class TestLogin: """登录场景测试用例""" @pytest.mark.parametrize("username,password,expect", [ ("admin", "admin123", "已登录"), ("admin", "wrong", "用户名或密码错误"), ("", "", "用户名或密码错误"), ]) def test_login_scenarios(self, driver, base_url, username, password, expect): driver.get(base_url) login_page = LoginPage(driver) if username == "admin" and password == "admin123": login_page.login(username, password) assert expect in driver.page_source else: login_page.login(username, password) assert expect in driver.page_source用例层干的事情非常纯粹:把页面对象的方法组合成一条业务场景,然后用断言验证结果。参数化用的三组数据分别覆盖了正常登录、密码错误、空数据三个场景。注意这里expect的断言用的都是driver.page_source里的文本,虽然不够优雅,但对于一个 mock 页面来说够用了,真实项目中建议在页面上加一个专门的元素(比如登录成功后的用户名)来断言,定位到具体元素再断言它的文本。不要对driver.page_source做空泛的断言,那样可能因为页面上静态资源加载不全导致误判。
参数化之所以把“输入错误密码”和“输入空密码”放在一起,是因为它们的预期都是一个“401 页面”。但值得注意的是,示例代码里login()方法没有处理“登录失败时页面是否停留在登录页”这个断言。更细化的用例可以这样写:登录失败后,仍然能找到USERNAME_INPUT元素,说明页面没有跳转,并且页面上出现了错误提示文本。这样的断言逻辑更接近真实业务场景。
5. 运行、报告与持续集成的两种做法:Hook 钩子和日志追踪
5.1 失败自动截图方案
用户最关心的往往是“用例挂了能不能让我一眼看到现场”。添加失败截图是一个能直接提升分析效率的功能。pytest 有pytest_runtest_makereport钩子,它会在每个测试方法执行完成后被调用,如果结果里failed是True,就调用 driver 的get_screenshot_as_file方法。截图保存的路径要按日期和用例名组织,比如reports/screenshots/20240912_test_login_失败原因.png。把截图路径放进日志,同时给 HTML 报告设置附件,就能在统一入口看到现场。
# testcases/conftest.py @pytest.hookimpl(hookwrapper=True) 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 is not None: screenshot_dir = Path("reports/screenshots") screenshot_dir.mkdir(parents=True, exist_ok=True) screenshot_path = screenshot_dir / f"{item.name}_{int(time.time())}.png" driver.save_screenshot(str(screenshot_path)) report.screenshot = str(screenshot_path)report.when的判断非常重要,pytest 一个用例会触发 setup、call、teardown 三个阶段,只需要在call阶段(也就是用例体本身执行完毕)判断失败。如果你不加这个判断,setup 出错时也可能尝试截图,而此时 driver 可能根本没有创建成功。item.funcargs.get("driver")这个取法可以拿到当前用例 fixture 的实参,比从item内部去翻 fixture 要直观得多。adding screenshot to report 的方式,配合 pytest-html 插件的--self-contained-html参数,能把截图嵌入 HTML 文件里,别人打开一个报告文件就能看到失败现场。
5.2 HTML 报告不是随便生成的
Selenium 自动化最不体面的结果是:用例跑完了,给别人看一个控制台输出。pytest-html 是目前最普及的报告方案。用法非常直接,运行命令加上参数即可:
pytest testcases -v --html=reports/report.html --self-contained-html--self-contained-html这个参数很有必要,它会把 CSS、JS 和截图都嵌入到单文件 HTML 里,方便直接分发和邮件附件。但要注意,pytest-html 的报告标题默认是你的测试目录名,想要自定义标题要加一行插件配置。更稳妥的做法是在pytest.ini里一次性把常用参数固定下来:
# pytest.ini [pytest] addopts = -v --tb=short --html=reports/report.html --self-contained-html testpaths = testcases--tb=short控制了失败堆栈的详细程度。默认的 long 模式会打印过多无关代码,特别是在 selenium 这种库的内部调用上非常冗长;short 模式只保留最关键的链条,排查定位失败问题足够用了。testpaths限定了 pytest 只去testcases/目录下收集用例,避免它去扫描pages/目录里那些以 test 开头的文件而产生误收集。
5.3 本地跑和 CI 跑要区分开来的两个细节
本地跑测试,和 CI 的 Jenkins / GitLab CI 里跑测试,最大的差异在两点。第一是浏览器需要无头模式,在服务器上没有显示器,如果不开启 headless 浏览器会直接启动失败。第二是驱动下载的稳定性,webdriver-manager 在 CI 里可能因为网络受限下载不了驱动,这时要在 CI 机器的镜像里预先下载好驱动并配置PATH环境变量。
如果团队用的是 Docker 跑测试,常见做法是直接使用现成的 Selenium 官方镜像,并把驱动问题抛在镜像外解决:
docker run -d --name chrome -p 4444:4444 -p 7900:7900 selenium/standalone-chrome然后用 Selenium 的 Remote WebDriver 方式连接这个固定地址。这样本地代码不变,只把create_driver里的webdriver.Chrome换成webdriver.Remote,构造参数里加一个command_executor指向http://localhost:4444/wd/hub。这种做法的额外收益是浏览器版本被固定在镜像里,不受本机 Chrome 自动升级影响,测试结果的可复现性大幅提升。但要注意镜像的拉取需要内网代理或镜像加速器,这一点是工程化落地时要提前评估的。
5.4 从运行日志里定位问题
最后再补一个日常排错的技巧。在框架里封装好日志模块之后,每次执行都要带着日志跑。pytest 运行后,日志输出到哪里?这是一个高频问题。建议在logger.py里同时往控制台和文件输出,日志文件按logs/20240912.log这样的格式每天一个:
# common/logger.py import logging from datetime import datetime def setup_logger(name: str = "auto") -> logging.Logger: logger = logging.getLogger(name) logger.setLevel(logging.INFO) fmt = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s") timestamp = datetime.now().strftime("%Y%m%d") fh = logging.FileHandler(f"logs/{timestamp}.log", encoding="utf-8") fh.setFormatter(fmt) ch = logging.StreamHandler() ch.setFormatter(fmt) logger.addHandler(fh) logger.addHandler(ch) return loggerlogger.py要注意.addHandler()不能重复调用,否则日志会打印两遍。如果你观察到一个用例的日志出现了两次,大概率是 logger 实例被多次创建,解决方案是在模块底部加这句:
logger = setup_logger()flie 和 console 双输出的价值在于:控制台实时看执行过程,文件留底。一旦测试结束,直接打开当天 log 文件搜索ERROR或者FAILED就能快速定位到失败用例。配合失败截图功能,基本不需要再手动打开浏览器去复现问题了。
6. 框架进阶的两个运用:复用登录态与失败重试机制
跑通了基础用例之后,框架真正面对复杂业务时还有两个绕不开的关键问题:登录态的最高效利用,以及用例稳定性不足时的自动重试。
6.1 复用登录态:让所有用例不再重复登录
大多数测试场景有一个共同的痛点:登录是最大的时间开销之一。一百个用例都从登录开始跑,光登录这一步就要占掉整个套件执行时间的三分之一。复用登录态并不是偷懒,而是合理利用浏览器的 profile 机制。
Selenium 在启动 Chrome 时可以指定一个 user-data-dir,浏览器会把 Cookie、LocalStorage、Session 都写到这个目录。第一次执行登录用例后,这个目录里已经存在有效会话;后续测试启动 Chrome 时直接加载该目录,浏览器就能直接处于登录状态,跳过登录步骤。以下是改造方案:
from selenium import webdriver def create_driver_with_profile(profile_dir: str = "/tmp/chrome_profile", headless: bool = False): """复用指定的 Chrome 用户数据目录,保留登录态""" options = webdriver.ChromeOptions() options.add_argument(f"--user-data-dir={profile_dir}") if headless: options.add_argument("--headless=new") options.add_argument("--no-sandbox") return webdriver.Chrome(options=options)用法上要注意,指定 user-data-dir 之后,不能再同时用默认的 profile。第一次执行时先手动访问被测系统并登录一次,然后把该目录保留下来,供后续运行挂载。但这种方法有一个非常容易出现的问题:--user-data-dir指定的路径如果被上一次残留的非正常关闭的浏览器进程锁定,Chrome 会启动失败;遇到这种情况排查时,先杀掉残留的 chromedriver 和 chrome 进程,删除该目录后重来。
另外要特别注意安全边界:登录态属于敏感数据,不要把 profile 目录提交到 Git 仓库,建议在.gitignore里显式忽略/tmp/chrome_profile或者类似目录。持续集成服务器上每个构建都用一个全新目录,不要在 CI 上复用 profile,否则很容易出现会话过期但用例仍然当作通过的情况。
6.2 失败重试机制:测试框架为稳定性兜底
自动化用例最讨厌的误报之一,是页面上某个接口超时,实际功能没变化,但用例失败了。没有重试机制的时候,只能人工确认后手动重跑;有重试机制,就能把这类波动自动消化。pytest-rerunfailures 插件是比较成熟的方案,用法极其简单:
pip install pytest-rerunfailures pytest testcases -v --reruns 2 --reruns-delay 3参数含义很直白:--reruns 2代表用例失败后最多重跑 2 次;--reruns-delay 3代表每次重跑间隔 3 秒。间隔不要太短,否则网络抖动还没恢复就重试,等于没重试。但注意重试机制只适用于偶发性的环境问题,如果断言本身写错了、定位器写错了,重试一万次也是失败。所以要把重试次数控制在 2-3 次之内,并且重试后的每次失败都要保留完整的日志和截图,方便确认到底是“环境原因自动恢复了”还是“功能真的坏了”。
结合这两点技巧,框架最终就形成了一个闭环:复用登录态提高速度,重试机制提升稳定性,报告与日志保证失败现场的可追溯性。真正验证框架好不好用的方式不是功能写得多炫,而是把执行时间拉长跑三天,看看每天的 Green Rate 和误报率。能把这两个指标稳定下来,这套基于 Python+Selenium 的框架才算是真正交付到了团队手里。
本文还有配套的精品资源,点击获取