Selenium+Pytest自动化测试框架搭建:从零构建企业级Web测试解决方案
1. 项目概述:为什么是Selenium+Pytest?
如果你正在为Web应用的回归测试、兼容性测试或者日常的冒烟测试而头疼,每次手动点点点不仅效率低下还容易出错,那么构建一个属于自己的自动化测试框架,可能就是你现在最需要投入精力去做的事情。在众多技术选型中,Selenium负责模拟浏览器操作,Pytest负责组织和管理测试用例,这两者的组合几乎成了Python自动化测试领域的“黄金搭档”。这个项目,就是要带你从零开始,手把手搭建一个结构清晰、易于维护、功能强大的Selenium+Pytest自动化测试框架。这不仅仅是写几个测试脚本,而是构建一个包含用例管理、数据驱动、失败重试、多浏览器支持、以及美观测试报告在内的完整工程体系。无论你是刚接触自动化测试的新手,还是想优化现有测试流程的测试开发工程师,这套实战方案都能为你提供直接的、可复现的参考。
2. 框架整体设计与核心思路拆解
2.1 为什么选择Selenium+Pytest这个组合?
在开始动手之前,我们先要搞清楚为什么是它们俩。市面上有很多工具,比如Playwright、Cypress等后起之秀,但Selenium+Pytest的组合依然拥有强大的生命力和广泛的社区基础。
Selenium WebDriver的核心价值在于其标准化和跨浏览器能力。它遵循W3C标准,这意味着你写的脚本理论上可以在Chrome、Firefox、Edge、Safari等主流浏览器上运行,对于需要做浏览器兼容性测试的项目来说,这是刚需。虽然像Playwright在性能和内置等待机制上可能有优势,但Selenium庞大的用户群、丰富的资料和稳定的API,对于团队协作和长期维护来说,是一个更稳妥的选择。它就像一把可靠的老钳子,可能不是最炫的,但你知道它一定能干活,并且出了问题很容易找到人一起解决。
Pytest则是一个远超unittest的测试框架。它的魅力在于“约定优于配置”和极强的可扩展性。你不需要像unittest那样必须继承某个类,一个以test_开头的函数就是一个测试用例。它的fixture机制是精髓所在,可以优雅地处理测试前置(如启动浏览器)、后置(如关闭浏览器)操作以及测试数据的注入。此外,丰富的插件生态(如pytest-html生成报告、pytest-rerunfailures失败重试、pytest-xdist分布式执行)让你能像搭积木一样构建强大的测试能力。Pytest让测试代码变得简洁而富有表达力。
这个组合的架构思路很清晰:Pytest作为组织和调度者,Selenium作为执行者。我们通过Pytest的fixture来管理WebDriver的生命周期,用Pytest的用例发现和参数化功能来驱动测试数据,最后用Pytest的插件来增强报告和稳定性。
2.2 框架分层目录结构设计
一个混乱的目录是项目腐化的开始。一个清晰的分层结构不仅能提升代码可读性,更便于团队协作和后期维护。这里我推荐一种经典的PO(Page Object)模式结合Pytest框架的分层目录结构,这也是我经过多个项目验证后觉得最实用的一种。
your_automation_framework/ ├── conftest.py # Pytest全局配置、共享fixture定义 ├── pytest.ini # Pytest主配置文件 ├── requirements.txt # 项目依赖包列表 │ ├── common/ # 公共层 │ ├── __init__.py │ ├── base_page.py # 页面基类,封装通用方法(如查找元素、点击) │ ├── webdriver_factory.py # WebDriver工厂,负责创建和管理不同浏览器驱动 │ └── logger.py # 自定义日志模块 │ ├── page_objects/ # 页面对象层(PO核心) │ ├── __init__.py │ ├── login_page.py # 登录页面对象 │ ├── home_page.py # 主页页面对象 │ └── ... # 其他页面对象 │ ├── test_cases/ # 测试用例层 │ ├── __init__.py │ ├── test_login.py # 登录相关测试用例 │ ├── test_search.py # 搜索相关测试用例 │ └── conftest.py # 测试用例层独有的fixture(可选) │ ├── test_data/ # 测试数据层 │ ├── __init__.py │ ├── login_data.yaml # YAML格式的登录测试数据 │ └── users.json # JSON格式的用户数据 │ ├── reports/ # 测试报告输出目录(.gitignore忽略) │ └── 2024-05-27_14-30-22.html │ └── screenshots/ # 失败截图输出目录(.gitignore忽略) └── test_login_error.png各层职责解析:
- common(公共层):存放与具体业务无关的底层工具。
base_page.py是所有页面对象的父类,封装了如find_element、click、input_text等通用操作,并加入智能等待、日志记录和失败截图。webdriver_factory.py根据配置决定是启动本地Chrome还是远程Grid节点,实现浏览器管理的解耦。 - page_objects(页面对象层):这是PO模式的核心。每个页面对应一个类(如
LoginPage),类中属性代表页面元素(如用户名输入框),类中方法代表页面操作(如login(username, password))。测试用例里不直接出现find_element_by_id这样的Selenium原生代码,而是调用LoginPage().login(“user”, “pass”),极大提升可维护性。当页面元素ID变更时,你只需要修改对应的页面对象类即可。 - test_cases(测试用例层):这里存放真正的Pytest测试文件。用例应该非常简洁,只包含测试步骤和断言,所有页面操作都委托给页面对象层。例如:
def test_login_success(login_page): login_page.login(“valid_user”, “valid_pass”); assert login_page.is_login_successful()。 - test_data(测试数据层):将测试数据从代码中分离出来。使用YAML或JSON文件管理数据,便于维护和进行数据驱动测试。Pytest的
@pytest.mark.parametrize装饰器可以方便地读取这些数据。 - reports & screenshots(报告与截图层):输出目录,存放每次运行的HTML报告和失败用例的截图,便于问题回溯。务必将其加入
.gitignore。
注意:
conftest.py可以存在于各层目录中,作用域不同。根目录的conftest.py中定义的fixture对整个项目生效,而test_cases目录下的conftest.py中的fixture只对该目录下的用例生效。这是Pytest一个非常灵活的特性。
3. 核心细节解析与实操要点
3.1 环境搭建与依赖管理:一步都不能错
万事开头难,环境搭建是第一步,也是最容易踩坑的一步。很多人在这里被驱动版本不匹配、环境变量等问题劝退。
1. 创建虚拟环境与安装依赖:强烈建议使用虚拟环境(如venv或conda)来隔离项目依赖。在项目根目录下执行:
# 创建虚拟环境 python -m venv venv # 激活虚拟环境 (Windows) venv\Scripts\activate # 激活虚拟环境 (MacOS/Linux) source venv/bin/activate # 安装核心依赖 pip install selenium pytest # 安装常用插件 pip install pytest-html pytest-rerunfailures pytest-xdist allure-pytest # 安装用于数据读写的库,推荐YAML,比JSON更易读 pip install pyyaml2. WebDriver驱动管理:这是Selenium的核心痛点。你需要下载与你的浏览器版本严格匹配的WebDriver(如chromedriver对应Chrome)。
- 最佳实践:使用
webdriver-manager。这个第三方库可以自动下载和管理正确版本的驱动,省去手动下载和配置环境变量的麻烦。
在代码中,可以这样使用:pip install webdriver-managerfrom selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium import webdriver service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service) - 手动管理:如果你身处内网或对版本有严格要求,仍需手动下载。记住驱动版本、浏览器版本、Selenium库版本三者需要兼容。将驱动放在项目目录下(如
drivers/)或系统PATH路径中。
3. Pytest基础配置 (pytest.ini):这个文件是Pytest的指挥中心,放在项目根目录。
[pytest] # 指定测试文件搜索路径 testpaths = test_cases # 指定文件名匹配规则 python_files = test_*.py # 指定类名匹配规则 python_classes = Test* # 指定函数名匹配规则 python_functions = test_* # 添加命令行默认参数 addopts = -v --html=reports/report.html --self-contained-html --reruns 1 --reruns-delay 2 # 设置日志格式和级别 log_cli = true log_cli_level = INFO log_cli_format = %(asctime)s [%(levelname)s] %(message)s--html=reports/report.html:使用pytest-html插件生成HTML报告。--self-contained-html:将CSS样式内嵌到HTML中,生成单个可分享的报告文件。--reruns 1 --reruns-delay 2:使用pytest-rerunfailures插件,失败用例自动重试1次,间隔2秒。这对付Web测试中的偶发性元素加载失败非常有效。
3.2 Page Object (PO) 模式深度实践
PO模式不是简单地把find_element语句封装到一个类里,其核心思想是将页面元素定位与业务操作分离,将测试用例与页面细节分离。
一个优秀的BasePage类应该包含什么?common/base_page.py是你的所有页面对象的“父亲”。它应该提供稳健的基础操作。
import logging from datetime import datetime from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.common.exceptions import TimeoutException, NoSuchElementException class BasePage: def __init__(self, driver): self.driver = driver self.logger = logging.getLogger(__name__) self.wait = WebDriverWait(driver, 10) # 显式等待10秒 def find_element(self, locator): """查找单个元素,加入显式等待和日志""" self.logger.info(f"正在查找元素: {locator}") try: element = self.wait.until(EC.presence_of_element_located(locator)) self.logger.info(f"元素查找成功: {locator}") return element except TimeoutException: self.logger.error(f"元素查找超时: {locator}") self._take_screenshot(“element_not_found”) raise def click(self, locator): """点击元素,点击前确保元素可点击""" element = self.find_element(locator) self.wait.until(EC.element_to_be_clickable(locator)) 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): """内部方法:截图并保存""" timestamp = datetime.now().strftime(“%Y%m%d_%H%M%S”) filename = f"screenshots/{name}_{timestamp}.png” self.driver.save_screenshot(filename) self.logger.info(f"截图已保存至: {filename}")实操心得:在
find_element中统一加入显式等待和日志,能从根本上减少因元素未加载完成导致的失败,并且让测试执行过程有迹可循。截图功能最好在基类的异常处理中调用,而不是在每个用例里写。
业务页面对象示例:page_objects/login_page.py继承自BasePage。
from selenium.webdriver.common.by import By from common.base_page import BasePage class LoginPage(BasePage): # 页面元素定位器,统一管理 USERNAME_INPUT = (By.ID, “username”) PASSWORD_INPUT = (By.ID, “password”) LOGIN_BUTTON = (By.XPATH, “//button[@type=‘submit']”) ERROR_MESSAGE = (By.CLASS_NAME, “alert-error”) def __init__(self, driver): super().__init__(driver) self.driver = driver def open(self, url): self.driver.get(url) self.logger.info(f“打开登录页面: {url}”) def login(self, username, password): """登录业务操作""" self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click(self.LOGIN_BUTTON) self.logger.info(f“尝试登录,用户名: {username}”) def get_error_message(self): """获取错误提示信息""" return self.get_text(self.ERROR_MESSAGE) def is_login_successful(self): """判断登录是否成功,这里需要根据实际成功后的页面特征来判断""" # 例如:检查是否跳转到首页,或出现用户菜单 # return “dashboard” in self.driver.current_url # 这是一个示例,实际逻辑需根据你的应用调整 try: self.find_element((By.ID, “user-menu”)) return True except: return False注意事项:定位器(如
(By.ID, “username”))一定要作为类属性单独定义,而不是直接写在方法里。这样一旦页面元素ID变更,你只需要修改这一个地方,所有用到该元素的方法都会自动生效。这是PO模式可维护性的关键。
4. 实操过程与核心环节实现
4.1 编写可维护的Pytest测试用例
有了稳固的底层和页面对象,编写测试用例就变成了一件清晰而愉快的事情。测试用例应该专注于“测试逻辑”,而不是“操作细节”。
一个完整的测试用例文件示例 (test_cases/test_login.py):
import pytest import yaml from page_objects.login_page import LoginPage # 从YAML文件加载测试数据 def load_login_data(): with open(‘test_data/login_data.yaml’, ‘r’, encoding=‘utf-8’) as f: data = yaml.safe_load(f) return data[‘test_cases’] class TestLogin: """登录功能测试集""" @pytest.mark.parametrize(“case_id, username, password, expected”, [ (“TC_LOGIN_001”, “admin”, “admin123”, “success”), (“TC_LOGIN_002”, “”, “admin123”, “用户名不能为空”), (“TC_LOGIN_003”, “admin”, “”, “密码不能为空”), (“TC_LOGIN_004”, “wrong”, “wrong”, “用户名或密码错误”), ]) def test_login_with_parametrize(self, browser_driver, case_id, username, password, expected): """使用pytest参数化进行数据驱动测试""" login_page = LoginPage(browser_driver) login_page.open(“https://your-app.com/login”) login_page.login(username, password) if expected == “success”: assert login_page.is_login_successful(), f“用例 {case_id} 登录成功断言失败” else: actual_error = login_page.get_error_message() assert expected in actual_error, f“用例 {case_id} 错误信息断言失败。预期: ‘{expected}’, 实际: ‘{actual_error}’” # 使用外部YAML文件进行更复杂的数据驱动 @pytest.mark.parametrize(“test_data”, load_login_data()) def test_login_with_yaml_data(self, browser_driver, test_data): """从YAML文件读取数据驱动测试""" login_page = LoginPage(browser_driver) login_page.open(test_data[‘url’]) login_page.login(test_data[‘credentials’][‘username’], test_data[‘credentials’][‘password’]) # ... 断言逻辑对应的YAML数据文件test_data/login_data.yaml:
test_cases: - case_id: “TC_LOGIN_YAML_001” description: “使用有效管理员账号登录” url: “https://your-app.com/login” credentials: username: “admin” password: “admin123” expected: “success” screenshot_on_failure: true - case_id: “TC_LOGIN_YAML_002” description: “使用已锁定账号登录” url: “https://your-app.com/login” credentials: username: “locked_user” password: “123456” expected: “账号已被锁定,请联系管理员” screenshot_on_failure: true关键点解析:
browser_driverfixture:这是测试用例的“燃料”。它由根目录的conftest.py提供,负责创建和销毁WebDriver实例。用例本身不关心浏览器如何启动和关闭。@pytest.mark.parametrize:这是Pytest数据驱动的利器。它允许你将多组测试数据直接绑定到一个测试函数上,Pytest会自动生成多条测试用例并分别执行。上面的例子展示了两种方式:直接在装饰器中定义列表,或者调用一个函数从外部文件(YAML/JSON)加载复杂数据。- 清晰的断言:断言是测试的灵魂。断言信息要明确,使用f-string将用例ID、预期值和实际值都包含在失败信息里,这样当用例失败时,你能一眼看出是哪条数据出了问题。
- 用例独立性:每个用例都应该是独立的,不依赖于其他用例的执行状态。这就是为什么
browser_driverfixture的默认作用域是function(每个用例函数执行一次)。确保用例开始时都从一个干净的会话状态(如未登录的首页)开始。
4.2 灵魂所在:conftest.py 与 Fixture 设计
conftest.py是Pytest框架的“配置中心”和“共享资源池”。在这里定义的fixture可以被项目中的所有测试用例使用。合理设计fixture是框架灵活性和稳定性的关键。
根目录下的conftest.py示例:
import pytest import logging from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from webdriver_manager.firefox import GeckoDriverManager # 配置日志 logging.basicConfig(level=logging.INFO, format=‘%(asctime)s - %(name)s - %(levelname)s - %(message)s’) def pytest_addoption(parser): """添加自定义命令行选项""" parser.addoption(“--browser”, action=“store”, default=“chrome”, help=“指定浏览器: chrome 或 firefox”) parser.addoption(“--headless”, action=“store_true”, default=False, help=“是否以无头模式运行”) @pytest.fixture(scope=“function”) def browser_driver(request): """核心Fixture:提供WebDriver实例,并确保测试结束后退出""" browser_name = request.config.getoption(“--browser”) is_headless = request.config.getoption(“--headless”) driver = None if browser_name.lower() == “chrome”: chrome_options = webdriver.ChromeOptions() if is_headless: chrome_options.add_argument(“--headless”) chrome_options.add_argument(“--no-sandbox”) # 解决Linux/Docker环境常见问题 chrome_options.add_argument(“--disable-dev-shm-usage”) # 解决共享内存问题 chrome_options.add_argument(“--window-size=1920,1080”) # 设置初始窗口大小 # 使用webdriver-manager自动管理驱动 service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=chrome_options) elif browser_name.lower() == “firefox”: firefox_options = webdriver.FirefoxOptions() if is_headless: firefox_options.add_argument(“--headless”) service = Service(GeckoDriverManager().install()) driver = webdriver.Firefox(service=service, options=firefox_options) else: raise ValueError(f“不支持的浏览器: {browser_name}”) driver.implicitly_wait(5) # 设置隐式等待(备用,优先使用显式等待) driver.maximize_window() # 最大化窗口 # 将driver对象传递给测试用例 yield driver # 测试结束后执行的清理工作 request.addfinalizer(driver.quit) # 更推荐使用 yield 后的代码,但 addfinalizer 是另一种确保退出的方式 # 实际上,使用 yield 后,下面的 quit 会在用例结束时执行 driver.quit() logging.info(“浏览器已关闭”) @pytest.fixture(scope=“function”) def login_page(browser_driver): """提供一个已初始化好的LoginPage对象""" from page_objects.login_page import LoginPage return LoginPage(browser_driver) @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """Hook函数:在测试用例执行后获取结果,用于失败时自动截图""" outcome = yield report = outcome.get_result() # 只关注用例调用阶段(setup, call, teardown中的call)的失败 if report.when == “call” and report.failed: # 尝试从item中获取driver对象 driver_fixture = item.funcargs.get(‘browser_driver’, None) if driver_fixture is not None: # 截图并作为附件添加到HTML报告(需要pytest-html支持) screenshot_path = f“screenshots/{item.name}_{report.when}.png” driver_fixture.save_screenshot(screenshot_path) # 将截图路径添加到报告extra中 if hasattr(report, ‘extra’): from pytest_html import extras report.extra.append(extras.image(screenshot_path))深度解析:
pytest_addoption:这个函数允许你定义自定义命令行参数。例如,你可以通过pytest --browser=firefox --headless来指定用Firefox无头模式运行所有测试。这为测试的灵活执行(如CI/CD流水线)提供了入口。browser_driverfixture:scope=“function”:这是默认值,意味着每个测试函数都会获得一个全新的driver实例。这保证了测试的隔离性。你也可以设置为scope=“session”让所有用例共用一个浏览器会话(更快,但用例间可能相互影响)。yield driver:这是fixture提供资源的核心语法。yield之前的代码是“设置”部分(启动浏览器),yield返回driver对象给测试用例使用,yield之后的代码是“清理”部分(关闭浏览器)。无论测试用例是否通过,yield后的代码都会执行,确保浏览器被正确关闭,避免进程残留。- 无头模式与选项:
--headless对于在服务器(无GUI环境)或CI/CD中运行测试至关重要。--no-sandbox和--disable-dev-shm-usage是解决Linux/Docker环境下Chrome常见崩溃问题的关键参数。
login_pagefixture:这是一个“业务层”的fixture。它依赖于browser_driver,并直接返回一个初始化好的LoginPage对象。在测试用例中,你可以直接声明def test_something(login_page):,然后使用login_page.login(...),让用例更加简洁。pytest_runtest_makereportHook:这是Pytest框架的一个钩子函数,允许你在测试生命周期的特定时刻插入自定义逻辑。这里我们用它来监听测试失败事件,并自动截图。截图会保存在screenshots/目录,并且如果你使用pytest-html,还可以通过extras将图片嵌入HTML报告,实现“报告里直接看失败截图”的效果,极大提升调试效率。
5. 常见问题与排查技巧实录
即使框架搭建得再完善,在实际运行中依然会遇到各种“坑”。下面是我在多年实践中总结的一些高频问题和解决思路。
5.1 元素定位失败:自动化测试的头号敌人
超过70%的自动化测试失败源于元素定位问题。错误信息通常是NoSuchElementException、TimeoutException或ElementNotInteractableException。
排查清单与解决方案:
| 问题现象 | 可能原因 | 解决方案与技巧 |
|---|---|---|
| 突然找不到之前能定位的元素 | 1. 页面加载未完成。 2. 元素在iframe或shadow DOM内。 3. 页面发生了跳转或重定向。 | 1.强化等待:将BasePage中的显式等待时间调长,或使用更精确的等待条件(如element_to_be_clickable)。2.检查iframe:使用 driver.switch_to.frame(frame_reference)切换到正确的iframe后再定位。3.检查Shadow DOM:使用 driver.execute_script执行JavaScript来穿透Shadow DOM定位元素。 |
元素可以找到,但点击或输入无效 (ElementNotInteractableException) | 1. 元素被遮挡(如弹窗、广告)。 2. 元素不可见或未启用( style=“display: none;”)。3. 需要滚动到元素可见区域。 | 1.处理遮挡:先关闭或等待遮挡物消失。可以尝试用JavaScript直接点击:driver.execute_script(“arguments[0].click();”, element)。2.等待元素可交互:使用 EC.element_to_be_clickable进行等待。3.滚动到视图: driver.execute_script(“arguments[0].scrollIntoView(true);”, element)。 |
| 定位器写对了,但偶尔失败 | 1. 网络波动或前端渲染性能问题。 2. 动态ID或类名(每次刷新会变)。 | 1.增加重试机制:使用pytest-rerunfailures插件全局重试失败用例。2.使用更稳定的定位策略:优先使用 id、name,其次是用XPath结合相对路径和元素属性(避免用绝对路径和索引)。例如://button[contains(@class, ‘submit-btn’) and text()=‘登录’]。 |
| 在Ajax动态加载的列表中定位元素 | 元素是动态添加到DOM中的,初始定位时不存在。 | 1.等待元素出现:使用EC.presence_of_all_elements_located等待一组元素出现。2.先等待加载完成:等待某个加载完成的标志性元素出现,再操作列表。 |
实操心得:遇到定位问题,不要急着改代码。首先,手动在浏览器的开发者工具(F12)中,用Console尝试你的XPath或CSS选择器是否能选中元素。其次,在测试失败时,利用我们Hook函数自动截取的截图,直观地看看到底是页面没加载出来,还是元素状态不对。最后,考虑在定位前加入一个短暂的
time.sleep(2)(仅用于临时调试)来区分是等待问题还是定位器本身问题。
5.2 测试报告优化与失败分析
生成的报告不好看,或者失败信息不清晰,会大大增加排查成本。
1. 解决Allure报告中用例标题换行问题:当你使用@pytest.mark.parametrize进行数据驱动测试时,如果参数值很长,生成的Allure报告中的用例标题可能会被挤得换行,影响阅读。解决方案是自定义用例标题。
import allure import pytest @pytest.mark.parametrize(“username, password”, [ (“very_long_username_value_that_might_cause_line_break”, “pass1”), (“user2”, “another_long_password_value_here”), ]) def test_login_with_different_users(username, password): # 使用 allure.dynamic.title 动态设置一个简洁的标题 allure.dynamic.title(f“登录测试 - 用户: {username[:10]}...”) # 截取用户名前10位 # ... 测试步骤 ...这样,在Allure报告中,用例标题就会显示为你设置的简洁版本,而不是完整的参数化长标题。
2. 丰富HTML报告内容:使用pytest-html插件时,可以通过Hook函数向报告中添加更多有用信息。
# 在 conftest.py 中 def pytest_html_report_title(report): report.title = “我的自动化测试报告” def pytest_html_results_table_header(cells): cells.insert(2, “<th>测试描述</th>”) cells.insert(3, “<th>用例ID</th>”) def pytest_html_results_table_row(report, cells): # 从测试用例的特定属性中获取描述和ID description = getattr(report, ‘description’, ‘’) case_id = getattr(report, ‘case_id’, ‘’) cells.insert(2, f“<td>{description}</td>”) cells.insert(3, f“<td>{case_id}</td>”) @pytest.hookimpl(hookwrapper=True) def pytest_runtest_makereport(item, call): outcome = yield report = outcome.get_result() # 将测试用例函数文档字符串作为描述 report.description = str(item.function.__doc__) # 假设我们用 @pytest.mark.case_id(‘TC_001’) 标记了用例ID markers = [mark for mark in item.iter_markers(name=“case_id”)] if markers: report.case_id = markers[0].args[0]然后在测试用例中,你可以用文档字符串和自定义标记来提供信息:
@pytest.mark.case_id(‘TC_LOGIN_001’) def test_admin_login(login_page): “”“验证管理员用户能够成功登录系统”“” # ... 测试步骤 ...5.3 稳定性提升与高级技巧
1. 处理弹窗和浏览器通知:有些网站会有登录弹窗(Basic Auth)、浏览器原生的alert/confirm/prompt弹窗,或者请求发送通知权限。
# 处理HTTP Basic认证弹窗(不是JS弹窗) # 方法:在URL中直接携带用户名和密码(注意安全风险,仅用于测试环境) driver.get(“https://username:password@your-site.com”) # 处理JS的Alert/Confirm/Prompt from selenium.webdriver.common.alert import Alert alert = Alert(driver) print(alert.text) # 获取弹窗文本 alert.accept() # 点击“确定” # alert.dismiss() # 点击“取消” # alert.send_keys(“input text”) # 向Prompt输入文本 # 阻止浏览器通知弹窗(在启动选项中添加) chrome_options.add_argument(“--disable-notifications”)2. 文件上传与下载:文件上传不能使用send_keys点击上传按钮,而是直接发送文件路径到<input type=“file”>元素。
# 找到文件上传的input元素 file_input = driver.find_element(By.XPATH, “//input[@type=‘file']”) # 直接发送绝对路径 file_input.send_keys(“/Users/yourname/Downloads/test_file.pdf”)对于文件下载,需要设置浏览器下载偏好,并指定下载目录。
prefs = { “download.default_directory”: “/path/to/your/download/folder”, “download.prompt_for_download”: False, “plugins.always_open_pdf_externally”: True } chrome_options.add_experimental_option(“prefs”, prefs)3. 使用Page Factory模式(可选):如果你觉得在PO类里写一堆find_element调用还是有点繁琐,可以考虑使用PageFactory模式(源自Java的Selenium,在Python中需借助selenium.webdriver.support.PageFactory)。它可以让你用注解的方式声明元素,并在初始化时自动查找。
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.webdriver.support.pagefactory import PageFactory class LoginPage: # 使用PageFactory的注解方式定义元素 username_input = (By.ID, “username”) password_input = (By.ID, “password”) login_button = (By.XPATH, “//button[@type=‘submit']”) def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) # 初始化页面元素 PageFactory.init_elements(driver, self) def login(self, username, password): # 直接使用 self.username_input, 但实际它是一个元组,需要配合find_element使用 # PageFactory模式在Python中不如Java中那么“自动”,通常需要自己封装。 # 更常见的做法是结合@property装饰器。 pass实际上,在Python中,更流行和灵活的还是我们之前介绍的显式在基类中封装find_element的方法。Page Factory可以作为了解,但并非必选。
构建一个健壮的Selenium+Pytest自动化测试框架,是一个从“能用”到“好用”,再到“稳定高效”的持续迭代过程。核心在于清晰的架构分层、稳健的底层封装、灵活的数据驱动和高效的排查手段。这个框架不是一成不变的,你可以根据自己项目的需求,引入API测试(用requests库)、数据库校验、或者与Jenkins/GitLab CI等持续集成工具对接,使其成为你质量保障体系中可靠的一环。记住,好的自动化测试框架,应该像一名不知疲倦、严谨细致的助手,把你从重复的机械劳动中解放出来,让你能更专注于更有价值的测试设计和问题挖掘。