从零开始搭建自动化测试框架:Python+Selenium实战教程(附完整代码)
从零构建一个面向未来的自动化测试框架:Python与Selenium的深度工程实践
最近和几位从功能测试转向自动化的朋友聊天,发现一个挺普遍的现象:大家看了不少教程,也照着敲了代码,但一到自己公司项目里,就不知道从何下手了。要么是脚本写得太“一次性”,跑完就扔;要么是维护成本高得吓人,页面改个按钮,一堆脚本全得重写。这让我想起自己刚入门时踩过的那些坑——自动化测试远不止是“用代码模拟点击”那么简单,它更像是在搭建一个可持续、可维护、高效率的质量保障工程体系。
今天,我们不谈那些泛泛的概念,直接动手,从最干净的环境开始,一步步搭建一个具备工业级水准的自动化测试框架。这个框架的核心是Python和Selenium,但我们的重点会放在框架设计思想、代码组织结构、以及如何应对真实项目的复杂性上。无论你是想提升个人技能,还是为团队引入自动化实践,这套从实战中提炼出来的方法论和代码,都能给你提供清晰的路径和可靠的起点。
1. 环境配置与工程初始化:奠定坚实的基础
很多教程一上来就pip install selenium,然后就开始写脚本。这当然能跑起来,但为后续的协作和扩展埋下了隐患。我们第一步要做的,是建立一个标准化、隔离且可复现的开发环境。
1.1 使用虚拟环境与依赖管理
绝对不要在系统全局Python环境里安装项目依赖。虚拟环境是你的第一个安全屏障。
# 创建项目目录并进入
mkdir auto_test_framework && cd auto_test_framework
# 创建虚拟环境,推荐使用venv(Python 3.3+内置)
python -m venv venv
# 激活虚拟环境
# 在Windows上:
venv\Scripts\activate
# 在macOS/Linux上:
source venv/bin/activate
激活后,你的命令行提示符通常会显示(venv),表明你正工作在虚拟环境中。接下来,我们使用requirements.txt文件来精确管理依赖。
# requirements.txt
selenium==4.15.0
pytest==7.4.3
pytest-html==4.1.1
pytest-xdist==3.5.0
webdriver-manager==4.0.1
allure-pytest==2.13.2
openpyxl==3.1.2
PyYAML==6.0.1
使用pip install -r requirements.txt一键安装所有依赖。这里有几个关键包值得说明:
- pytest: 我们选择它而非unittest作为测试运行器,因为它更灵活、插件生态丰富。
- webdriver-manager: 它能自动下载和管理浏览器驱动(如ChromeDriver),彻底告别手动下载和路径配置的烦恼。
- allure-pytest: 用于生成强大、可视化的测试报告。
提示:将
requirements.txt纳入版本控制(如Git),确保任何团队成员都能快速搭建起完全一致的环境。
1.2 项目目录结构设计
混乱的目录结构是项目腐化的开始。下面这个结构经过了多个项目的检验,清晰且易于扩展。
auto_test_framework/
├── configs/ # 配置文件
│ ├── config.yaml # 主配置文件
│ └── test_data.yaml # 测试数据文件
├── drivers/ # 浏览器驱动(备用,通常由webdriver-manager管理)
├── logs/ # 日志文件目录(.gitignore)
├── reports/ # 测试报告目录(.gitignore)
│ ├── html/
│ └── allure/
├── src/ # 框架核心源码
│ ├── core/ # 核心模块
│ │ ├── __init__.py
│ │ ├── base_page.py # 页面基类
│ │ ├── webdriver_factory.py # 驱动工厂
│ │ └── logger.py # 日志模块
│ ├── pages/ # 页面对象模型
│ │ ├── __init__.py
│ │ ├── login_page.py
│ │ └── dashboard_page.py
│ └── utils/ # 工具函数
│ ├── __init__.py
│ ├── file_reader.py
│ └── wait_utils.py
├── tests/ # 测试用例
│ ├── __init__.py
│ ├── conftest.py # pytest共享夹具配置
│ ├── test_login.py
│ └── test_dashboard.py
├── .gitignore
├── pytest.ini # pytest配置文件
├── requirements.txt
└── README.md
这个结构的关键在于分离关注点。src里放的是可重用的框架代码,tests里是具体的测试用例,configs管理配置,reports和logs是产出物。坚持这个约定,项目再大也不会乱。
2. 框架核心层设计:打造可复用的引擎
有了好的结构,接下来我们填充核心代码。这部分是框架的“发动机”,决定了脚本的健壮性和可维护性。
2.1 智能驱动管理:WebDriver工厂模式
直接在每个测试用例里创建和关闭WebDriver是低效且容易出错的。我们采用工厂模式来统一管理驱动的生命周期。
# src/core/webdriver_factory.py
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from selenium.webdriver.firefox.service import Service as FirefoxService
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
from src.core.logger import get_logger
logger = get_logger(__name__)
class WebDriverFactory:
"""WebDriver工厂类,负责创建和管理浏览器实例"""
@staticmethod
def get_driver(browser_name="chrome", headless=False, options=None):
"""
获取WebDriver实例
:param browser_name: 浏览器类型,支持 'chrome', 'firefox', 'edge'
:param headless: 是否无头模式运行
:param options: 自定义浏览器选项列表
:return: WebDriver实例
"""
driver = None
try:
if browser_name.lower() == "chrome":
chrome_options = webdriver.ChromeOptions()
if headless:
chrome_options.add_argument("--headless=new") # 新版无头模式
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--disable-dev-shm-usage")
chrome_options.add_argument("--window-size=1920,1080")
# 添加自定义选项
if options:
for arg in options:
chrome_options.add_argument(arg)
# 使用webdriver-manager自动管理驱动
service = ChromeService(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=chrome_options)
elif browser_name.lower() == "firefox":
firefox_options = webdriver.FirefoxOptions()
if headless:
firefox_options.add_argument("--headless")
# ... 其他Firefox配置
service = FirefoxService(GeckoDriverManager().install())
driver = webdriver.Firefox(service=service, options=firefox_options)
else:
raise ValueError(f"不支持的浏览器类型: {browser_name}")
driver.implicitly_wait(10) # 设置隐式等待
driver.maximize_window()
logger.info(f"成功创建 {browser_name} 浏览器驱动实例")
return driver
except Exception as e:
logger.error(f"创建WebDriver失败: {e}")
raise
这个工厂类的好处是,我们可以在一个地方统一控制所有浏览器的创建逻辑。想加个新浏览器?改这里就行。想统一加个代理?也在这里配置。
2.2 页面对象模型(Page Object)的增强基类
经典的PO模式是把页面元素和操作封装成类。我们可以更进一步,创建一个智能基类,内置常用的等待、日志和异常处理。
# src/core/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, StaleElementReferenceException
from src.core.logger import get_logger
import time
logger = get_logger(__name__)
class BasePage:
"""所有页面对象的基类"""
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(self.driver, timeout=15, poll_frequency=0.5)
def find_element(self, locator, timeout=None):
"""查找单个元素,支持显式等待"""
wait_obj = self.wait if timeout is None else WebDriverWait(self.driver, timeout)
try:
element = wait_obj.until(EC.presence_of_element_located(locator))
logger.debug(f"定位到元素: {locator}")
return element
except TimeoutException:
logger.error(f"元素定位超时: {locator}")
raise
def click(self, locator, timeout=None):
"""点击元素,增加重试机制应对StaleElement异常"""
for i in range(3): # 重试3次
try:
element = self.find_element(locator, timeout)
element.click()
logger.info(f"点击元素: {locator}")
return
except StaleElementReferenceException:
logger.warning(f"元素状态过期,第{i+1}次重试: {locator}")
time.sleep(0.5)
continue
raise StaleElementReferenceException(f"元素 {locator} 在多次重试后仍不可用")
def input_text(self, locator, text, clear_first=True):
"""向输入框输入文本"""
element = self.find_element(locator)
if clear_first:
element.clear()
element.send_keys(text)
logger.info(f"向元素 {locator} 输入文本: {text}")
def get_text(self, locator):
"""获取元素文本"""
element = self.find_element(locator)
text = element.text
logger.debug(f"获取元素 {locator} 的文本: {text}")
return text
def is_element_visible(self, locator, timeout=5):
"""判断元素是否可见"""
try:
WebDriverWait(self.driver, timeout).until(
EC.visibility_of_element_located(locator)
)
return True
except TimeoutException:
return False
这个基类封装了Selenium原生的、不那么友好的API,提供了更健壮、更易用的方法。比如click方法内置了重试机制,能有效应对单页应用(SPA)中常见的元素状态刷新问题。
2.3 配置与数据驱动:让测试更灵活
硬编码的测试数据和配置是框架僵化的主要原因。我们用YAML文件来管理它们。
# configs/config.yaml
environment: &default_env
base_url: "https://demo.testfire.net"
browser: "chrome"
headless: false
implicit_wait: 10
explicit_wait: 15
test:
screenshot_on_failure: true
screenshot_dir: "./reports/screenshots/"
allure_report_dir: "./reports/allure/"
logging:
level: "INFO"
file_path: "./logs/automation.log"
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# 不同环境的配置覆盖
staging:
<<: *default_env
base_url: "https://staging.demo.testfire.net"
production:
<<: *default_env
base_url: "https://app.testfire.net"
headless: true
# src/utils/file_reader.py
import yaml
import json
import os
from src.core.logger import get_logger
logger = get_logger(__name__)
class ConfigReader:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(ConfigReader, cls).__new__(cls)
cls._instance._load_config()
return cls._instance
def _load_config(self):
config_path = os.path.join(os.path.dirname(__file__), '../../configs/config.yaml')
with open(config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
# 默认使用development环境,可通过环境变量覆盖
self.env = os.getenv('TEST_ENV', 'environment')
logger.info(f"加载配置完成,当前环境: {self.env}")
def get(self, key, default=None):
"""获取配置项,支持点分隔符如 'test.screenshot_on_failure'"""
keys = key.split('.')
value = self.config.get(self.env, {})
for k in keys:
if isinstance(value, dict):
value = value.get(k)
else:
return default
return value if value is not None else default
这样,我们只需要在运行测试前设置一个环境变量TEST_ENV(如export TEST_ENV=staging),就能轻松切换整套测试环境,无需修改任何代码。
3. 编写健壮且可维护的测试用例
框架搭好了,现在我们来写真正的测试用例。这里的关键是用例要清晰、独立、且易于定位问题。
3.1 使用pytest夹具(Fixture)管理测试生命周期
pytest的fixture功能非常强大,能优雅地处理测试前置和后置操作。
# tests/conftest.py
import pytest
from src.core.webdriver_factory import WebDriverFactory
from src.core.logger import setup_logging
from src.utils.file_reader import ConfigReader
import allure
# 读取配置
config = ConfigReader()
@pytest.fixture(scope="session")
def app_config():
"""提供全局配置的fixture"""
return config
@pytest.fixture(scope="function") # 每个测试函数一个独立的driver
def driver(app_config):
"""创建和销毁WebDriver的fixture"""
browser = app_config.get("browser")
headless = app_config.get("headless")
# 创建驱动
driver_instance = WebDriverFactory.get_driver(
browser_name=browser,
headless=headless
)
# 访问基础URL
base_url = app_config.get("base_url")
driver_instance.get(base_url)
yield driver_instance # 将driver提供给测试用例使用
# 测试结束后执行清理
if driver_instance:
driver_instance.quit()
@pytest.fixture(scope="function")
def login_page(driver, app_config):
"""提供已初始化的登录页面对象"""
from src.pages.login_page import LoginPage
return LoginPage(driver)
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""钩子函数,用于在测试失败时自动截图并附加到Allure报告"""
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
# 获取driver fixture
driver_fixture = item.funcargs.get('driver')
if driver_fixture:
# 截图并保存
screenshot_path = f"./reports/screenshots/{item.name}_{call.start}.png"
driver_fixture.save_screenshot(screenshot_path)
# 将截图附加到Allure报告
allure.attach.file(
screenshot_path,
name="失败截图",
attachment_type=allure.attachment_type.PNG
)
这个conftest.py文件是pytest的魔力所在。driver fixture确保了每个测试都有干净的浏览器环境;login_page fixture让页面对象能直接注入测试用例;而最后的钩子函数实现了测试失败自动截图,这对调试来说是无价之宝。
3.2 一个完整的测试用例示例
现在,我们可以用非常简洁、易读的方式编写测试了。
# tests/test_login.py
import pytest
import allure
from src.utils.file_reader import ConfigReader
config = ConfigReader()
@allure.feature("用户认证模块")
@allure.story("登录功能")
class TestLogin:
"""登录功能测试集"""
@allure.title("使用有效凭证成功登录")
@allure.severity(allure.severity_level.BLOCKER) # 阻塞级别缺陷
def test_successful_login(self, login_page):
"""
测试用例:验证使用正确的用户名和密码可以成功登录系统
预期结果:登录后跳转到仪表盘页面,显示欢迎信息
"""
# 准备测试数据
username = config.get("test_data.valid_username")
password = config.get("test_data.valid_password")
# 执行测试步骤
with allure.step("1. 在登录页面输入用户名和密码"):
login_page.enter_username(username)
login_page.enter_password(password)
with allure.step("2. 点击登录按钮"):
dashboard_page = login_page.click_login_button()
with allure.step("3. 验证登录成功,跳转到仪表盘"):
welcome_text = dashboard_page.get_welcome_message()
assert "欢迎" in welcome_text
assert username in welcome_text
with allure.step("4. 验证用户菜单可见"):
assert dashboard_page.is_user_menu_visible()
@allure.title("使用无效密码登录失败")
@allure.severity(allure.severity_level.CRITICAL)
@pytest.mark.parametrize("username, password, expected_error", [
("admin", "wrongpass", "密码错误"),
("nonexist", "anypass", "用户不存在"),
("", "anypass", "用户名不能为空"),
])
def test_failed_login(self, login_page, username, password, expected_error):
"""
参数化测试:验证各种无效登录场景
"""
login_page.enter_username(username)
login_page.enter_password(password)
login_page.click_login_button()
# 验证错误提示信息
actual_error = login_page.get_error_message()
assert expected_error in actual_error
# 验证仍然停留在登录页面
assert login_page.is_login_form_visible()
这个测试用例展示了几个优秀实践:
- 清晰的用例描述:函数名和文档字符串说明了测试目的。
- 使用Allure装饰器:
@allure.feature、@allure.story、@allure.step让测试报告极具可读性。 - 参数化测试:用
@pytest.mark.parametrize一次覆盖多个边界情况。 - 断言明确:断言语句清晰表达了预期结果。
- 页面对象封装:所有UI操作都通过页面对象完成,测试逻辑非常干净。
4. 高级主题:让框架更强大
基础框架跑起来后,我们可以考虑一些高级特性,进一步提升自动化测试的效率和价值。
4.1 并行测试执行
当测试用例成百上千时,串行执行会非常耗时。pytest-xdist插件可以轻松实现并行化。
# 使用2个worker并行执行测试
pytest tests/ -n 2
# 自动检测CPU核心数
pytest tests/ -n auto
# 并行执行并生成HTML报告
pytest tests/ -n auto --html=reports/html/report.html --self-contained-html
在conftest.py中,我们需要确保fixture能正确处理并行场景。通常,将driver fixture的scope设为function(每个测试独立)或session(整个会话共享)都能与xdist良好配合,取决于你是否需要测试间的完全隔离。
4.2 集成持续集成(CI)流水线
自动化测试只有集成到CI/CD流程中,才能发挥最大价值。这里给出一个GitHub Actions的配置示例。
# .github/workflows/test.yml
name: 自动化测试
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: 设置Python环境
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: 安装依赖
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: 运行自动化测试
env:
TEST_ENV: staging # 使用测试环境配置
run: |
# 运行测试并生成Allure结果
pytest tests/ -n auto --alluredir=reports/allure-results
- name: 生成并发布Allure报告
if: always() # 即使测试失败也生成报告
uses: simple-elf/allure-report-action@master
with:
allure_results: reports/allure-results
allure_report: reports/allure-report
keep_reports: 20
- name: 上传Allure报告为工件
uses: actions/upload-artifact@v3
if: always()
with:
name: allure-report
path: reports/allure-report
这个工作流会在每次推送到主分支或创建拉取请求时自动运行测试,并生成漂亮的Allure报告。团队可以通过报告快速了解本次代码变更的质量影响。
4.3 测试数据管理策略
测试数据管理是个容易被忽视但至关重要的问题。我们推荐分层管理策略:
| 数据类型 | 存储方式 | 示例 | 管理要点 |
|---|---|---|---|
| 环境配置数据 | YAML配置文件 | 数据库连接串、API端点、超时时间 | 按环境隔离,敏感信息使用环境变量 |
| 静态测试数据 | YAML/JSON文件 | 产品分类、国家列表、固定用户角色 | 版本控制,与用例同步维护 |
| 动态测试数据 | 运行时生成 | 唯一用户名、临时订单号、随机邮件 | 使用Faker库生成,测试后清理 |
| 敏感测试数据 | 密钥管理服务 | 真实支付密钥、第三方API令牌 | 绝对不存入代码库,使用CI变量注入 |
一个动态数据生成的工具类示例:
# src/utils/data_generator.py
from faker import Faker
import random
import string
from datetime import datetime, timedelta
fake = Faker('zh_CN') # 使用中文数据
class TestDataGenerator:
@staticmethod
def random_email():
return fake.email()
@staticmethod
def random_phone():
# 生成符合格式的中国手机号
return f"1{random.randint(30, 89)}{random.randint(1000, 9999)}{random.randint(1000, 9999)}"
@staticmethod
def random_string(length=8):
"""生成随机字符串,常用于用户名等"""
letters = string.ascii_letters
return ''.join(random.choice(letters) for _ in range(length))
@staticmethod
def future_date(days=7):
"""生成未来某天的日期,用于测试有效期等场景"""
return (datetime.now() + timedelta(days=days)).strftime("%Y-%m-%d")
@staticmethod
def create_user_data():
"""生成一套完整的用户测试数据"""
return {
"username": f"autotest_{TestDataGenerator.random_string(6)}",
"password": "Test@123456",
"email": TestDataGenerator.random_email(),
"phone": TestDataGenerator.random_phone(),
"real_name": fake.name(),
"address": fake.address()
}
在测试用例中,你可以这样使用:
def test_register_new_user(self, register_page):
# 动态生成测试数据,避免重复和冲突
user_data = TestDataGenerator.create_user_data()
register_page.fill_registration_form(user_data)
register_page.submit_form()
# 断言注册成功
assert register_page.is_success_message_displayed()
# 测试后,可以通过API或数据库清理测试数据
# cleanup_test_user(user_data['username'])
这种策略确保了测试的独立性和可重复性,避免了因测试数据冲突导致的“假失败”。
5. 常见问题与调试技巧
即使有了完善的框架,在实际编写和维护测试脚本时,还是会遇到各种问题。分享几个我踩过坑后总结的实用技巧。
5.1 元素定位失败:不只是找对选择器
元素定位是UI自动化中最常见的问题。除了使用正确的选择器,还要考虑:
1. 动态内容与等待策略 单页应用(SPA)中,元素可能异步加载。不要只依赖隐式等待,要结合显式等待。
# 不好的做法:直接操作,可能因元素未加载而失败
driver.find_element(By.ID, "dynamic-button").click()
# 好的做法:等待元素可点击
from selenium.webdriver.support.expected_conditions import element_to_be_clickable
button_locator = (By.ID, "dynamic-button")
wait = WebDriverWait(driver, 10)
button = wait.until(element_to_be_clickable(button_locator))
button.click()
2. 处理iframe和Shadow DOM 如果元素在iframe或Shadow DOM内,需要先切换到正确的上下文。
# 切换到iframe
iframe = driver.find_element(By.TAG_NAME, "iframe")
driver.switch_to.frame(iframe)
# 现在可以操作iframe内的元素了
# ...
# 操作完成后切换回主文档
driver.switch_to.default_content()
# 处理Shadow DOM(较新的API)
shadow_host = driver.find_element(By.CSS_SELECTOR, "custom-element")
shadow_root = shadow_host.shadow_root
inner_element = shadow_root.find_element(By.CSS_SELECTOR, ".inner-class")
3. 使用相对定位和多种策略备用 不要过度依赖单一属性,特别是那些容易变化的类名。
# 在页面对象中定义灵活的元素定位器
class LoginPage(BasePage):
# 主定位器
USERNAME_INPUT = (By.ID, "username")
# 备用定位器(当ID变化时使用)
USERNAME_INPUT_ALT = (By.CSS_SELECTOR, "input[name='username']")
USERNAME_INPUT_ALT2 = (By.XPATH, "//form//input[@type='text']")
def enter_username(self, username):
"""输入用户名,尝试多种定位策略"""
locators = [self.USERNAME_INPUT, self.USERNAME_INPUT_ALT, self.USERNAME_INPUT_ALT2]
for locator in locators:
try:
self.input_text(locator, username)
return
except:
continue
raise Exception("无法定位用户名输入框")
5.2 测试不稳定性(Flaky Tests)的应对
不稳定的测试(有时过有时不过)是自动化测试的毒瘤。以下方法可以显著减少这种情况:
1. 增加智能等待,减少硬性等待
避免使用time.sleep(),它不可靠且低效。
# 反模式
time.sleep(5) # 固定等待5秒
element.click()
# 改进:等待特定条件
def wait_for_page_load(driver, timeout=30):
"""等待页面完全加载"""
WebDriverWait(driver, timeout).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
# 或者等待某个特定元素出现
wait.until(EC.presence_of_element_located((By.ID, "page-loaded-marker")))
2. 重试机制 对于已知的不稳定操作,实现重试逻辑。
# src/utils/retry_utils.py
import time
from functools import wraps
from src.core.logger import get_logger
logger = get_logger(__name__)
def retry_on_failure(max_attempts=3, delay=1, exceptions=(Exception,)):
"""重试装饰器"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except exceptions as e:
if attempt == max_attempts:
logger.error(f"操作失败,已达最大重试次数 {max_attempts}: {e}")
raise
logger.warning(f"第 {attempt} 次尝试失败,{delay}秒后重试: {e}")
time.sleep(delay)
return wrapper
return decorator
# 使用示例
class PaymentPage(BasePage):
@retry_on_failure(max_attempts=3, delay=2, exceptions=(TimeoutException,))
def complete_payment(self):
"""完成支付操作,失败时自动重试"""
self.click(self.PAY_BUTTON)
# 等待支付成功提示
self.wait.until(EC.visibility_of_element_located(self.SUCCESS_MESSAGE))
3. 隔离测试环境 确保每个测试都是独立的,不依赖其他测试的状态或数据。
@pytest.fixture(scope="function")
def clean_user_account(driver, app_config):
"""确保测试开始前用户账户处于初始状态"""
# 1. 通过API清理测试数据
user_api.cleanup_test_data(test_user_id)
# 2. 清除浏览器本地存储(针对前端状态)
driver.execute_script("window.localStorage.clear();")
driver.execute_script("window.sessionStorage.clear();")
# 3. 删除cookies
driver.delete_all_cookies()
yield
# 测试后也可以做清理
user_api.cleanup_test_data(test_user_id)
5.3 有效的调试与日志记录
当测试失败时,详细的日志和调试信息能帮你快速定位问题。
1. 结构化日志记录 我们在框架中已经集成了日志模块,关键是要在适当的地方记录适当的信息。
# 在页面对象方法中添加有意义的日志
def click_submit(self):
"""点击提交按钮"""
try:
self.click(self.SUBMIT_BUTTON)
self.logger.info("成功点击提交按钮")
# 记录点击后的页面状态
current_url = self.driver.current_url
self.logger.debug(f"点击后当前URL: {current_url}")
except Exception as e:
self.logger.error(f"点击提交按钮失败: {e}")
# 记录失败时的页面状态,便于调试
self.logger.error(f"失败时页面标题: {self.driver.title}")
self.logger.error(f"失败时页面源码片段: {self.driver.page_source[:500]}...")
raise
2. 使用浏览器开发者工具集成 在调试模式下,可以启用浏览器开发者工具,或者直接通过WebDriver执行JavaScript来获取更多信息。
# 在测试失败时执行诊断脚本
def diagnose_page_state(driver):
"""诊断当前页面状态"""
diagnostics = {}
# 获取控制台错误
console_logs = driver.get_log('browser')
diagnostics['console_errors'] = [log for log in console_logs if log['level'] == 'SEVERE']
# 检查是否有未捕获的JavaScript错误
js_errors = driver.execute_script("return window.JS_ERRORS || [];")
diagnostics['js_errors'] = js_errors
# 检查关键元素是否存在
diagnostics['key_elements'] = {}
key_selectors = ['#app', '.main-content', 'body.loaded']
for selector in key_selectors:
try:
element = driver.find_element(By.CSS_SELECTOR, selector)
diagnostics['key_elements'][selector] = {
'exists': True,
'visible': element.is_displayed()
}
except:
diagnostics['key_elements'][selector] = {'exists': False}
return diagnostics
# 在测试失败时调用
def test_something(driver):
try:
# 测试逻辑...
pass
except Exception as e:
# 记录诊断信息
diag = diagnose_page_state(driver)
logger.error(f"测试失败诊断信息: {diag}")
raise
3. 视频录制与网络请求监控 对于复杂问题,有时需要更详细的记录。
# 使用第三方库录制测试视频(需额外安装)
# pip install pytest-selenium-video
# 在pytest.ini中配置
[pytest]
selenium_video_recording = True
video_recording_dir = ./reports/videos/
video_recording_failed_only = True
# 或者监控网络请求
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
# 启用性能日志
caps = DesiredCapabilities.CHROME
caps['goog:loggingPrefs'] = {'performance': 'ALL'}
driver = webdriver.Chrome(desired_capabilities=caps)
# 获取网络请求日志
logs = driver.get_log('performance')
for log in logs:
message = json.loads(log['message'])['message']
if message['method'] == 'Network.requestWillBeSent':
url = message['params']['request']['url']
logger.debug(f"网络请求: {url}")
这些调试技巧不是每次都要用,但当你遇到棘手的问题时,它们就是你的工具箱。关键是在框架设计阶段就预留好这些扩展点,而不是等到问题发生时才临时抱佛脚。
最后,关于维护成本,我个人的经验是:定期重构测试代码和框架。每过几个月,回顾一下哪些模式工作得好,哪些地方变得难以维护。自动化测试代码也是产品代码,需要同样的关注和重构。有时候,花一下午时间优化一个基类方法,能为未来节省几十个小时的调试时间。
更多推荐


所有评论(0)