Python UI自动化测试实战:pytest+Selenium+Allure+PO模式避坑指南
1. 项目概述:从零到一构建健壮的UI自动化测试体系
做UI自动化测试,尤其是用Python技术栈的,pytest、Selenium、Allure加上PO模式,几乎成了标配组合。听起来很美,但真正上手去搭框架、写用例、跑起来,你会发现坑是一个接一个。浏览器版本不兼容、元素定位飘忽不定、测试报告不够直观、用例维护成本高企……这些问题我都经历过。今天这篇内容,不是教科书式的框架搭建教程,而是我作为一线测试开发,在多个项目中反复踩坑、填坑后,总结出的一份“避坑合集”。我会围绕 pytest+Selenium+Allure+PO模式 这个核心组合,把那些官方文档不会写、新手教程容易忽略的细节、陷阱和最佳实践,掰开揉碎了讲清楚。目标是让你不仅能搭起来,更能用得稳、维护得轻松,真正发挥自动化测试的价值。
2. 核心架构与设计思路拆解
2.1 为什么是这“四件套”?
在开始填坑之前,得先明白我们为什么选这套组合拳,而不是单个工具或者别的框架。这决定了我们后续所有设计的出发点。
Pytest :它不仅仅是一个测试运行器。其强大的Fixture机制(用于测试前置后置条件)、参数化、丰富的插件生态(如 pytest-html , pytest-xdist 分布式执行),以及灵活的断言(直接用 assert ,无需记复杂的 self.assertEqual ),让它成为组织和管理测试用例的绝佳选择。它的可读性和扩展性远超 unittest 。
Selenium :Web UI自动化的“事实标准”。它提供了操控浏览器的底层协议支持(WebDriver),兼容Chrome、Firefox、Edge等主流浏览器。虽然近年来有Playwright、Cypress等后起之秀,但Selenium的生态成熟度、社区支持度和语言绑定(Python、Java等)的稳定性,对于企业级、需要长期维护的项目来说,依然是稳妥的首选。
Allure :测试报告界的“颜值担当”。它生成的交互式HTML报告,能清晰展示测试套件层级、用例状态、步骤详情、附件(截图、日志)、历史趋势等。这对于快速定位失败原因、向团队展示测试结果、进行测试分析至关重要。相比 pytest-html 生成的静态报告,Allure在信息呈现和问题排查效率上优势明显。
PO模式 :这不是一个具体工具,而是一种设计模式。它的核心思想是将 页面对象 和 测试逻辑 分离。每个页面(或页面片段)封装成一个类,页面的元素定位和基本操作(如点击、输入)作为这个类的方法。测试用例则通过调用这些页面对象的方法来组合业务流程。这样做最大的好处是 可维护性 :当页面UI发生变化时,你只需要修改对应的页面对象类中的元素定位,而不需要去浩如烟海的测试用例脚本里一个个修改。
把这四者结合起来,就形成了一个分工明确、各司其职的自动化测试体系:Pytest负责调度和管理,Selenium负责执行动作,Allure负责呈现结果,PO模式负责组织代码结构。理解了这个,后续的很多“坑”其实都是为了让这个体系协作得更顺畅。
2.2 项目目录结构设计:清晰是维护的第一道防线
一个混乱的目录结构是项目腐化的开始。下面是我经过多个项目迭代后,认为比较合理的一种结构,它严格遵循了PO模式的思想,并考虑了扩展性。
your_ui_auto_project/
├── conftest.py # Pytest全局Fixture定义,如驱动初始化、失败截图
├── pytest.ini # Pytest配置文件,配置命令行默认参数、标记等
├── requirements.txt # 项目依赖包列表
├── common/ # 公共模块
│ ├── __init__.py
│ ├── base_page.py # 所有页面对象的基类,封装公共方法
│ ├── webdriver_factory.py # 浏览器驱动工厂,负责创建和配置WebDriver实例
│ └── logger.py # 自定义日志模块
├── page_objects/ # 页面对象层
│ ├── __init__.py
│ ├── login_page.py # 登录页面
│ ├── home_page.py # 主页
│ └── ... (其他页面)
├── test_cases/ # 测试用例层
│ ├── __init__.py
│ ├── test_login.py # 登录相关测试用例
│ ├── test_search.py # 搜索相关测试用例
│ └── conftest.py # 测试用例层特有的Fixture(可选)
├── test_data/ # 测试数据层
│ ├── login_data.yaml # YAML格式的登录测试数据
│ └── ...
├── reports/ # 测试报告输出目录(通常.gitignore)
│ ├── allure-results/ # Allure原始结果文件
│ └── allure-report/ # Allure生成的HTML报告
└── screenshots/ # 失败截图存放目录(通常.gitignore)
设计理由 :
- 分离关注点 :
page_objects、test_cases、test_data完全分离,符合PO模式。 - 公共抽象 :
common目录存放可复用的代码,如BasePage减少了重复代码。 - 配置集中 :
conftest.py和pytest.ini集中管理框架配置。 - 输出隔离 :
reports和screenshots存放运行时产物,方便清理和归档,也便于在.gitignore中忽略。
注意 :
conftest.py可以有多份,作用域不同。项目根目录下的conftest.py是全局的,test_cases目录下的只对该目录内的用例生效。合理使用可以精细化管理Fixture。
3. 核心细节解析与实操要点
3.1 Selenium WebDriver的“隐形”大坑:驱动管理与浏览器选项
这是新手最容易栽跟头的地方。问题通常表现为:脚本在本地能跑,换台机器或CI/CD环境就失败;或者浏览器总是弹出“正受到自动测试软件控制”的提示,甚至被目标网站识别为自动化脚本而拒绝服务。
坑1:浏览器与WebDriver版本不匹配 Selenium的工作原理是,你的脚本通过 WebDriver 协议与一个特定的 浏览器驱动 (如 chromedriver )通信,再由这个驱动去控制真实的浏览器。因此,浏览器版本、驱动版本、Selenium库版本三者必须兼容。
- 避坑方案 :
- 锁定版本 :在
requirements.txt中明确指定selenium的版本。 - 使用WebDriver Manager :这是终极解决方案。安装
webdriver-manager库,它会在运行时自动检测系统已安装的浏览器版本,并下载匹配的驱动。彻底告别手动下载和配置环境变量。pip install webdriver-manager - 在代码中应用:
from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.options import Options options = Options() # 添加一些常用选项,见下文 service = Service(ChromeDriverManager().install()) driver = webdriver.Chrome(service=service, options=options)
- 锁定版本 :在
坑2:浏览器特征过于明显 默认启动的Chrome浏览器会在标题栏、 navigator.webdriver 属性等处暴露自己是受自动化控制,容易被反爬机制识别。
- 避坑方案 :通过
ChromeOptions添加实验性参数来隐藏特征。options = Options() # 隐藏“正受到自动测试软件控制”提示 options.add_experimental_option("excludeSwitches", ["enable-automation"]) options.add_experimental_option('useAutomationExtension', False) # 更进一步的隐藏(模拟普通用户) options.add_argument("--disable-blink-features=AutomationControlled") # 无头模式运行(不显示GUI,常用于CI环境) # options.add_argument("--headless") # 禁用沙盒和/dev/shm使用,解决部分Linux环境问题 options.add_argument("--no-sandbox") options.add_argument("--disable-dev-shm-usage") # 禁用密码保存弹窗等 prefs = { "credentials_enable_service": False, "profile.password_manager_enabled": False } options.add_experimental_option("prefs", prefs) driver = webdriver.Chrome(service=service, options=options) # 执行CDP命令,覆盖navigator.webdriver属性 driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", { "source": """ Object.defineProperty(navigator, 'webdriver', { get: () => undefined }); """ })实操心得 :不是所有网站都需要这么严格的隐藏。对于内部系统测试,可能只需要前两行。对于爬虫或测试有反爬的公开网站,则需要更完整的配置。
--headless模式虽然节省资源,但调试时不直观,且有些交互(如复杂的鼠标悬停)可能表现不同,建议调试阶段用有头模式。
坑3:元素定位不稳定与等待机制 NoSuchElementException 是UI自动化中最常见的异常。原因无非两种:定位表达式写错了,或者元素还没加载出来。
- 避坑方案 :
- 优先使用稳定定位器 :优先级建议为
id>name>css selector>xpath。xpath功能强大但性能稍差且易受页面结构微小变动影响,尽量使用相对路径和属性组合,避免绝对路径。 - 强制使用显式等待 : 绝对不要 用
time.sleep()!这是最糟糕的实践。使用Selenium提供的WebDriverWait配合expected_conditions。from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By # 错误示范 import time time.sleep(5) # 固定等待,浪费时间且不可靠 # 正确示范:等待最多10秒,直到元素可点击 element = WebDriverWait(driver, 10).until( EC.element_to_be_clickable((By.ID, "submit-button")) ) element.click() - 封装智能等待方法到BasePage :在每个页面对象的操作中都写一遍
WebDriverWait很繁琐。我通常在BasePage中封装一个find_element方法,集成显式等待。
这样,在具体的页面对象里,操作就变得非常简洁和稳定:# common/base_page.py class BasePage: def __init__(self, driver): self.driver = driver self.wait = WebDriverWait(driver, 10) # 全局等待超时时间 def find_element(self, locator): """查找单个元素,集成显式等待""" return self.wait.until(EC.presence_of_element_located(locator)) def click_element(self, locator): """点击元素,等待其可点击""" element = self.wait.until(EC.element_to_be_clickable(locator)) element.click() def input_text(self, locator, text): """输入文本,先清空再输入""" element = self.find_element(locator) element.clear() element.send_keys(text)# page_objects/login_page.py from selenium.webdriver.common.by import By from common.base_page import BasePage class LoginPage(BasePage): # 定位器统一管理 USERNAME_INPUT = (By.ID, "username") PASSWORD_INPUT = (By.NAME, "password") LOGIN_BUTTON = (By.CSS_SELECTOR, ".btn-login") def login(self, username, password): self.input_text(self.USERNAME_INPUT, username) self.input_text(self.PASSWORD_INPUT, password) self.click_element(self.LOGIN_BUTTON)
- 优先使用稳定定位器 :优先级建议为
3.2 Pytest Fixture的精髓:驱动生命周期的掌控
Pytest的Fixture是管理测试依赖(如WebDriver实例)的神器。用得好,代码清晰且高效;用不好,会出现驱动未初始化、用例间状态污染等问题。
坑:驱动实例如何安全地创建和销毁? 我们需要一个Fixture来负责 driver 的初始化和退出,并确保每个测试用例都在一个干净的浏览器会话中开始。
- 避坑方案 :在根目录的
conftest.py中定义driverFixture,并合理设置作用域。# conftest.py import pytest from selenium import webdriver from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager from selenium.webdriver.chrome.options import Options @pytest.fixture(scope="function") # 关键:作用域设为“函数” def driver(): """ 为每个测试函数创建一个独立的driver实例。 这是最安全的方式,避免了用例间的状态干扰。 """ options = Options() # ... (添加你的浏览器选项配置) service = Service(ChromeDriverManager().install()) driver_instance = webdriver.Chrome(service=service, options=options) driver_instance.maximize_window() # 默认最大化窗口 driver_instance.implicitly_wait(5) # 设置全局隐式等待(辅助,不能替代显式等待) yield driver_instance # 将driver实例提供给测试用例 # 测试函数执行完毕后,执行清理工作 driver_instance.quit()-
scope=”function”:这是UI测试的推荐做法。每个测试用例都从一个新的浏览器会话开始,完全独立,虽然启动稍慢,但保证了测试的隔离性和可靠性。scope=”class”或”module”会导致用例共用浏览器,前一个用例的操作(如登录状态、浏览器缓存)可能影响后一个用例。 -
yield:yield之前是setup(初始化),yield之后是teardown(清理)。driver_instance通过yield传递给测试函数。 -
.quit()vs.close():一定要用.quit()。.close()只关闭当前标签页,而.quit()会关闭所有窗口并终止WebDriver会话,释放资源。
-
如何在测试用例中使用这个Fixture? 非常简单,只需要将 driver 作为测试函数的参数即可。
# test_cases/test_login.py
class TestLogin:
def test_login_success(self, driver): # 传入driver fixture
from page_objects.login_page import LoginPage
login_page = LoginPage(driver)
login_page.load("https://example.com/login") # 假设BasePage有load方法
login_page.login("valid_user", "valid_pass")
# ... 添加断言,验证登录成功
assert "dashboard" in driver.current_url
3.3 Allure报告的美化与问题定位
Allure报告默认已经不错,但我们可以通过添加步骤、附件、严重级别等,让它成为真正的调试利器。
坑:报告里只有冰冷的“Pass/Fail”,出错了不知道具体哪一步、页面长什么样。
- 避坑方案 :使用Allure的装饰器和方法增强报告。
-
添加测试步骤 :使用
@allure.step装饰器,可以将一个函数或方法标记为测试步骤,在报告中清晰展示。import allure from page_objects.login_page import LoginPage class TestLogin: @allure.step("打开登录页面") def open_login_page(self, driver): driver.get("https://example.com/login") return LoginPage(driver) @allure.step("使用用户名'{username}'和密码登录") def perform_login(self, login_page, username, password): login_page.login(username, password) def test_login_success(self, driver): login_page = self.open_login_page(driver) self.perform_login(login_page, "test_user", "123456") # ... 断言这样,报告中会展示“打开登录页面”和“使用用户名‘test_user’和密码登录”这两个步骤,一目了然。
-
失败时自动截图并附加到报告 :这是 最重要的 调试功能。我们可以通过扩展Pytest的
conftest.py来实现。# conftest.py import allure import pytest from datetime import datetime @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """ Hook函数,用于获取每个测试用例的执行结果。 当用例失败时,自动截图并附加到Allure报告。 """ outcome = yield report = outcome.get_result() # 只关注用例调用阶段(即执行测试函数本身)的失败 if report.when == "call" and report.failed: # 从测试用例的fixture中获取driver对象 driver_fixture = item.funcargs.get('driver') if driver_fixture: # 截图并保存为二进制数据 screenshot = driver_fixture.get_screenshot_as_png() # 以附件形式添加到Allure报告 allure.attach( screenshot, name=f"screenshot_{datetime.now().strftime('%Y%m%d_%H%M%S')}", attachment_type=allure.attachment_type.PNG ) # 同时也可以附加页面源代码,方便分析 page_source = driver_fixture.page_source allure.attach( page_source, name="page_source", attachment_type=allure.attachment_type.TEXT )这个Hook函数会在每个测试用例执行后调用。如果用例失败了(
report.failed),并且这个用例使用了driverfixture,它就会自动截取当前浏览器屏幕和页面源码,作为附件添加到Allure报告中。你无需在每一个测试用例的try...except里写截图代码。 -
设置测试特性、严重级别 :使用
@allure.feature,@allure.story,@allure.severity来对测试用例进行分类和分级,方便在报告中过滤和查看。import allure @allure.feature("用户登录模块") @allure.story("成功登录场景") @allure.severity(allure.severity_level.CRITICAL) # 阻塞级别 class TestLoginSuccess: def test_login_with_correct_credential(self, driver): ...
-
4. 实操过程与核心环节实现
4.1 完整项目初始化与依赖安装
让我们从头开始,搭建这个框架。假设项目名为 ui_auto_demo 。
-
创建项目目录 :
mkdir ui_auto_demo && cd ui_auto_demo -
创建虚拟环境(强烈推荐) :隔离项目依赖。
python -m venv venv # Windows激活 venv\Scripts\activate # macOS/Linux激活 source venv/bin/activate -
创建
requirements.txt文件 :# 核心测试框架与驱动管理 pytest>=7.0.0 selenium>=4.10.0 webdriver-manager>=4.0.0 # 测试报告 allure-pytest>=2.13.0 # 测试数据管理(可选,YAML易于阅读) pyyaml>=6.0 # HTTP请求库,用于可能的接口校验或准备数据 requests>=2.28.0 -
安装依赖 :
pip install -r requirements.txt -
安装Allure命令行工具 :Allure报告生成需要Java环境和命令行工具。
- 安装Java JDK 8+ 。
- 下载Allure :从 Allure官网 下载,解压并将其
bin目录添加到系统PATH环境变量。 - 验证安装:
allure --version。
-
按照第2.2节的目录结构 ,创建所有文件夹和空的
__init__.py文件。
4.2 编写核心基础类与页面对象
1. 编写 common/base_page.py : 这是所有页面对象的基石,封装了最常用的操作和等待逻辑。
# common/base_page.py
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import allure
class BasePage:
"""页面对象基类"""
def __init__(self, driver, timeout=10):
self.driver = driver
self.wait = WebDriverWait(driver, timeout)
self.timeout = timeout
def find_element(self, locator):
"""查找单个元素(等待出现)"""
with allure.step(f"查找元素: {locator}"):
return self.wait.until(EC.presence_of_element_located(locator))
def find_elements(self, locator):
"""查找多个元素"""
with allure.step(f"查找元素列表: {locator}"):
return self.wait.until(EC.presence_of_all_elements_located(locator))
def click_element(self, locator):
"""点击元素(等待可点击)"""
with allure.step(f"点击元素: {locator}"):
element = self.wait.until(EC.element_to_be_clickable(locator))
element.click()
return element
def input_text(self, locator, text):
"""向元素输入文本"""
with allure.step(f"向元素 {locator} 输入文本: {'*' * len(text) if 'password' in str(locator).lower() else text}"):
element = self.find_element(locator)
element.clear()
element.send_keys(text)
return element
def get_text(self, locator):
"""获取元素的文本"""
with allure.step(f"获取元素文本: {locator}"):
element = self.find_element(locator)
return element.text
def is_element_visible(self, locator, timeout=None):
"""判断元素是否可见"""
wait = WebDriverWait(self.driver, timeout or self.timeout)
try:
wait.until(EC.visibility_of_element_located(locator))
return True
except:
return False
def load(self, url):
"""访问URL"""
with allure.step(f"访问URL: {url}"):
self.driver.get(url)
2. 编写一个具体的页面对象,例如 page_objects/login_page.py :
# page_objects/login_page.py
from selenium.webdriver.common.by import By
from common.base_page import BasePage
import allure
class LoginPage(BasePage):
"""登录页面对象"""
# 定位器统一在此定义,便于维护
INPUT_USERNAME = (By.ID, "username")
INPUT_PASSWORD = (By.ID, "password")
BUTTON_LOGIN = (By.XPATH, "//button[@type='submit']")
ALERT_ERROR = (By.CLASS_NAME, "alert-error")
LINK_FORGOT_PWD = (By.LINK_TEXT, "忘记密码?")
def __init__(self, driver):
super().__init__(driver)
@allure.step("登录操作 - 用户名: {username}")
def login(self, username, password):
"""执行登录"""
self.input_text(self.INPUT_USERNAME, username)
self.input_text(self.INPUT_PASSWORD, password)
self.click_element(self.BUTTON_LOGIN)
# 登录后,通常返回下一个页面对象,如主页
# from page_objects.home_page import HomePage
# return HomePage(self.driver)
@allure.step("获取错误提示信息")
def get_error_message(self):
"""获取登录失败时的错误提示"""
if self.is_element_visible(self.ALERT_ERROR):
return self.get_text(self.ALERT_ERROR)
return ""
4.3 编写测试用例与数据驱动
1. 编写测试用例 test_cases/test_login.py :
# test_cases/test_login.py
import pytest
import allure
from page_objects.login_page import LoginPage
@allure.feature("用户认证模块")
class TestLogin:
"""登录功能测试集"""
@allure.story("成功登录")
@allure.severity(allure.severity_level.BLOCKER)
def test_login_success(self, driver):
"""测试使用正确凭据登录成功"""
login_page = LoginPage(driver)
# 假设我们有一个测试用的登录页
login_page.load("https://the-internet.herokuapp.com/login")
login_page.login("tomsmith", "SuperSecretPassword!")
# 断言:登录成功后应跳转到安全页
assert "/secure" in driver.current_url
assert "Secure Area" in driver.title
@allure.story("登录失败 - 用户名错误")
@allure.severity(allure.severity_level.CRITICAL)
@pytest.mark.parametrize("username, password, expected_error", [
("wrong_user", "SuperSecretPassword!", "Your username is invalid!"),
("", "SuperSecretPassword!", "Your username is invalid!"),
])
def test_login_failure_wrong_username(self, driver, username, password, expected_error):
"""测试使用错误用户名登录失败"""
login_page = LoginPage(driver)
login_page.load("https://the-internet.herokuapp.com/login")
login_page.login(username, password)
# 断言:应显示预期的错误信息
actual_error = login_page.get_error_message()
assert expected_error in actual_error
@allure.story("登录失败 - 密码错误")
def test_login_failure_wrong_password(self, driver):
"""测试使用错误密码登录失败"""
login_page = LoginPage(driver)
login_page.load("https://the-internet.herokuapp.com/login")
login_page.login("tomsmith", "wrongpass")
actual_error = login_page.get_error_message()
assert "Your password is invalid!" in actual_error
2. 使用外部测试数据(YAML示例) : 对于更复杂的数据驱动测试,可以将测试数据分离到YAML文件中。
# test_data/login_data.yaml
success:
username: "tomsmith"
password: "SuperSecretPassword!"
expected_url: "/secure"
expected_title: "Secure Area"
failure_cases:
- username: "wrong_user"
password: "SuperSecretPassword!"
expected_error: "Your username is invalid!"
- username: "tomsmith"
password: "wrong_pass"
expected_error: "Your password is invalid!"
- username: ""
password: ""
expected_error: "Your username is invalid!"
然后在测试用例中读取:
import yaml
import pytest
def load_login_data():
with open('test_data/login_data.yaml', 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
data = load_login_data()
@pytest.mark.parametrize("case", data['failure_cases'])
def test_login_failure_data_driven(driver, case):
login_page = LoginPage(driver)
login_page.load("https://the-internet.herokuapp.com/login")
login_page.login(case['username'], case['password'])
assert case['expected_error'] in login_page.get_error_message()
4.4 运行测试并生成Allure报告
-
运行测试 :在项目根目录下执行。
# 运行所有测试 pytest # 运行特定标记的测试 pytest -m "critical" # 运行特定文件 pytest test_cases/test_login.py # 运行并输出简洁报告 pytest -vPytest会自动发现以
test_开头的文件和函数,并使用conftest.py中定义的Fixture。 -
生成Allure报告 :
# 第一步:运行测试并生成Allure原始结果数据(--clean-alluredir 先清空历史结果) pytest --alluredir=./reports/allure-results --clean-alluredir # 第二步:根据原始数据生成HTML报告 allure generate ./reports/allure-results -o ./reports/allure-report --clean # 第三步:打开报告(本地查看) allure open ./reports/allure-report执行后,会在
./reports/allure-report目录下生成一个完整的HTML报告,用浏览器打开index.html即可查看。
5. 常见问题与排查技巧实录
即使框架搭好了,在日常执行中还是会遇到各种“妖魔鬼怪”。下面是我总结的一些高频问题及解决方法。
5.1 元素定位相关
问题1:脚本昨天还能跑,今天就报 NoSuchElementException 了。
- 排查 :
- 检查页面是否真的变了 :手动打开浏览器,F12检查元素,看定位器(如ID、Class)是否还在。前端框架(如React、Vue)动态生成的ID可能每次都会变。
- 检查是否有iframe :目标元素是否在
<iframe>里?如果在,需要先切换上下文。# 切换到iframe iframe = driver.find_element(By.TAG_NAME, "iframe") driver.switch_to.frame(iframe) # 在iframe内操作元素... # 操作完成后切回主文档 driver.switch_to.default_content() - 检查是否有新窗口/标签页 :点击后是否打开了新窗口?需要切换窗口句柄。
original_window = driver.current_window_handle # 点击打开新窗口的操作... # 等待新窗口出现 WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2)) # 切换到新窗口 for window_handle in driver.window_handles: if window_handle != original_window: driver.switch_to.window(window_handle) break # 在新窗口操作... # 关闭新窗口并切回 driver.close() driver.switch_to.window(original_window) - 检查等待是否足够 :网络慢或前端渲染慢可能导致元素加载慢。尝试增加显式等待时间,或使用更合适的等待条件(如
visibility_of代替presence_of)。
问题2: ElementClickInterceptedException 或 ElementNotInteractableException
- 排查 :
- 元素被遮挡 :可能有弹窗、悬浮层、另一个元素盖在了上面。尝试先关闭或移开遮挡物。
- 元素不可见/不在视口内 :有些元素需要滚动到可视区域才能交互。
element = driver.find_element(...) driver.execute_script("arguments[0].scrollIntoView(true);", element) element.click() - 元素状态不可点击 :例如,按钮有
disabled属性。需要等待其变为可点击状态,这正是EC.element_to_be_clickable要解决的。
5.2 测试稳定性与性能
问题:用例执行时快时慢,偶尔超时失败。
- 排查与优化 :
- 优化等待策略 :
- 减少/取消隐式等待 :全局隐式等待
driver.implicitly_wait()会为所有find_element操作增加额外时间,可能与显式等待冲突或拖慢速度。建议设置为一个较小的值(如2-5秒)或直接设为0,完全依赖显式等待。 - 使用更精确的显式等待条件 :不要总是用
presence_of_element_located,根据场景选择。例如,等待按钮可点击用element_to_be_clickable,等待元素可见用visibility_of_element_located,等待元素消失用invisibility_of_element_located。
- 减少/取消隐式等待 :全局隐式等待
- 清理浏览器状态 :对于
scope=”function”的Fixture,每个用例都是新会话,问题不大。但如果使用scope=”session”,需要在关键操作后清理cookies或localStorage。driver.delete_all_cookies() driver.execute_script("window.localStorage.clear();") - 使用
pytest-xdist进行并行测试 :当用例数量多时,可以并行执行以缩短总耗时。pip install pytest-xdist pytest -n auto # 自动检测CPU核心数并行注意 :并行时,确保测试用例之间没有依赖,且资源(如测试账号、测试数据)不会冲突。Fixture的
scope需要仔细设计,通常driverFixture不能是session级别的。
- 优化等待策略 :
5.3 Allure报告相关
问题:Allure报告没有生成,或者没有截图/步骤信息。
- 排查 :
- 检查
--alluredir参数 :确保运行pytest时正确指定了--alluredir目录。 - 检查Hook函数 :确保
pytest_runtest_makereport这个Hook函数正确写在了conftest.py中,并且逻辑正确(特别是判断report.failed和获取driverfixture的部分)。 - 检查Allure装饰器 :
@allure.step装饰器需要加在函数或方法上,直接加在类上无效。步骤描述支持使用函数的参数,如@allure.step(“登录: {username}”)。 - 清理历史结果 :有时旧的结果文件会导致生成失败。使用
--clean-alluredir参数或在生成报告时使用--clean选项。
- 检查
5.4 环境与配置
问题:在CI/CD服务器(如Jenkins, GitLab CI)上跑不起来。
- 排查 :
- 浏览器与驱动 :CI服务器通常是无GUI的Linux环境。必须使用 无头模式 ,并添加
--no-sandbox和--disable-dev-shm-usage参数(见3.1节)。确保webdriver-manager能正常下载驱动(网络通畅)。 - 依赖安装 :在CI脚本中,务必先
pip install -r requirements.txt。 - Allure报告集成 :CI中需要安装Allure命令行工具,并在流水线步骤中执行
allure generate和allure serve(或归档allure-report目录)。 - 资源路径 :代码中的相对路径(如读取
test_data/login_data.yaml)在CI中可能基于不同的工作目录。建议使用os.path模块构建绝对路径。import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) DATA_FILE = os.path.join(BASE_DIR, "test_data", "login_data.yaml")
- 浏览器与驱动 :CI服务器通常是无GUI的Linux环境。必须使用 无头模式 ,并添加
一个实用的调试技巧:在关键点打印页面源码或截图到文件 当CI上失败且Allure附件还不足以分析时,可以在代码中临时添加将页面源码或截图保存到文件的操作,以便下载查看。
def debug_save_page(driver, name="debug"):
import os
debug_dir = "debug_output"
os.makedirs(debug_dir, exist_ok=True)
# 保存截图
driver.save_screenshot(os.path.join(debug_dir, f"{name}.png"))
# 保存页面源码
with open(os.path.join(debug_dir, f"{name}.html"), "w", encoding="utf-8") as f:
f.write(driver.page_source)
print(f"Debug info saved to {debug_dir}/{name}.*")
在怀疑出问题的操作后调用这个函数,然后将 debug_output 目录作为CI产物归档。
更多推荐
所有评论(0)