Python+Selenium自动化测试框架搭建:从零到一实现高效UI测试
1. 项目概述:为什么我们需要一个“简单”的测试框架?
最近在带团队做项目,测试同学天天跟我抱怨,说回归测试的工作量太大了,每次发版前都要手动点点点,枯燥不说,还容易漏测。我一看,这不行啊,效率太低,而且人肉测试的稳定性也堪忧。于是,我决定带着他们搞一个自动化测试框架。目标很明确: 简单、好用、能快速落地 。我们不追求大而全的“银弹”,而是要一个团队成员(包括刚入行的测试新人)都能快速上手,能切实解决当前重复性功能验证问题的工具。
这就是“简单实现一个Python+Selenium的自动化测试框架”这个项目的由来。它本质上是一个 脚手架 ,帮你把自动化测试中那些繁琐、重复的准备工作(比如浏览器驱动管理、元素定位封装、测试报告生成、用例组织)都打包好,让你能专注于编写具体的测试逻辑。用Python+Selenium是因为这对测试同学来说学习曲线最平缓,生态也最成熟。这个框架能做什么?简单说,就是把你从“打开浏览器->输入网址->点击按钮->断言结果”这一系列手动操作中解放出来,用代码自动执行,并且生成清晰的测试报告,告诉你哪些用例过了,哪些挂了,为什么挂。
适合谁来参考?如果你是测试工程师,正苦于手工回归;如果你是开发,想为自己的模块补充一些自动化冒烟测试;甚至如果你是项目负责人,想提升团队的交付质量和效率,这个内容都值得一看。我们不会讲太深奥的设计模式,就从最实用的角度出发,一步步搭出一个能立刻用起来的框架。
2. 框架整体设计与核心思路拆解
在动手写代码之前,得先想清楚我们要做个什么东西。一个自动化测试框架,核心任务是把测试用例执行起来,并给出明确的结果。基于这个目标,我把它拆解成几个关键模块,这也是我们框架的骨架。
2.1 核心模块与职责划分
一个哪怕再简单的框架,也需要清晰的层次。我设计的结构主要分为四层,从上到下,职责逐渐细化:
- 测试用例层 :这是用户(测试工程师)主要工作的地方。他们在这里用自然语言(结合代码)描述测试场景,比如“用户登录成功”、“添加商品到购物车”。这一层应该尽量简洁,只关心业务逻辑和测试数据。
- 测试逻辑层 :这一层提供“积木”。我们把常用的页面操作(点击、输入、获取文本)和业务流(登录、退出)封装成一个个函数或类方法。用例层直接调用这些“积木”来搭建测试场景。这样做的好处是,当页面元素发生变化时,通常只需要修改这一层的某个“积木”,而不是去改每一个用例。
- 驱动与工具层 :这是框架的“发动机”和“工具箱”。负责管理WebDriver(控制浏览器的核心)、读取配置文件(如测试URL、浏览器类型)、生成测试报告、处理日志等。这些是支撑性的功能,为上层提供稳定的服务。
- 基础配置层 :存放所有环境相关的配置,比如
config.ini文件、测试数据文件(如JSON、Excel)、日志存放路径等。实现配置与代码分离,不同环境(测试、预生产)切换起来非常方便。
这个分层结构的思想是“高内聚、低耦合”。用例不管浏览器怎么启动的,逻辑层不管报告怎么生成的,各司其职,维护和扩展起来就轻松很多。
2.2 技术选型背后的考量:为什么是Python + Selenium + Pytest?
市面上工具很多,我选择这个组合是基于以下几点实实在在的考虑:
- Python :语法简洁,接近自然语言,对测试人员极其友好。庞大的第三方库(
pytest,requests,openpyxl)让解决任何问题(数据驱动、报告生成)都像“拼积木”一样简单。团队学习成本最低。 - Selenium :Web自动化测试的“事实标准”。支持所有主流浏览器,API成熟稳定,社区资源丰富。遇到问题,几乎总能搜到解决方案。对于Web UI自动化来说,它是目前最稳妥的选择。
- Pytest :这是我框架的“测试执行引擎”。它比Python自带的
unittest更强大、更灵活。pytest的夹具(fixture)机制,能优雅地解决测试前置(如初始化浏览器)和后置(如关闭浏览器、截图)操作。它的断言写法更直观(直接用assert),测试发现规则更智能,插件生态(如生成allure报告)更是杀手锏。
注意 :不要试图自己从头造轮子去管理用例执行、前置后置条件和报告。
Pytest已经把这些做得非常好了,我们的框架应该是基于Pytest进行扩展和封装,而不是替代它。
2.3 目录结构规划:好的开始是成功的一半
代码还没写,我们先定好目录结构。清晰的目录能让项目一目了然,方便团队协作。我建议的目录结构如下:
automation_framework/
├── configs/ # 配置层
│ ├── config.ini # 主配置文件
│ └── test_data.json # 测试数据文件
├── logs/ # 日志目录(运行时生成)
├── reports/ # 测试报告目录(运行时生成)
├── screenshots/ # 失败用例截图目录(运行时生成)
├── drivers/ # 浏览器驱动目录
│ ├── chromedriver(.exe)
│ └── geckodriver(.exe)
├── page_objects/ # 逻辑层:页面对象
│ ├── __init__.py
│ ├── base_page.py # 基类,封装通用操作
│ ├── login_page.py # 登录页面
│ └── main_page.py # 主页
├── test_cases/ # 用例层
│ ├── __init__.py
│ ├── conftest.py # pytest共享夹具配置
│ ├── test_login.py # 登录相关测试用例
│ └── test_cart.py # 购物车相关测试用例
├── utilities/ # 工具层
│ ├── __init__.py
│ ├── logger.py # 日志工具
│ ├── config_reader.py # 配置读取工具
│ └── report_generator.py # 报告生成工具(可整合allure)
└── requirements.txt # 项目依赖包列表
这个结构不是一成不变的,你可以根据项目复杂度调整。但核心思想是: 配置、用例、页面对象、工具、输出物 ,各归其位。
3. 核心细节解析与实操要点
框架的骨架搭好了,现在我们开始填充血肉。这一部分,我们会深入几个最核心的模块,看看它们具体如何实现,以及实现过程中有哪些“坑”需要避开。
3.1 灵魂所在:BasePage基类的设计与封装
BasePage 是整个页面对象模型(Page Object Model, POM)的基石。它的核心思想是,将页面元素定位和常用操作封装起来,让具体的页面类(如 LoginPage )继承它,从而获得所有能力,并只需关注自己特有的元素和操作。
为什么要用POM? 想象一下,如果100个测试用例里都用 driver.find_element(By.ID, “username”).send_keys(“admin”) 来输入用户名。一旦这个输入框的ID变了,你就需要修改这100个地方。而POM模式下,你只需要在 LoginPage 类里修改一次 username_input 这个属性的定位方式。维护成本天差地别。
下面是一个实战中提炼出的 BasePage 类示例,包含了我认为最必要的封装:
# page_objects/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, NoSuchElementException
import utilities.logger as log
import os
class BasePage:
"""所有页面对象的基类,封装通用Web操作和等待机制"""
def __init__(self, driver):
self.driver = driver
self.log = log.get_logger() # 获取日志记录器
self.wait = WebDriverWait(self.driver, 10) # 显式等待,超时10秒
def open(self, url):
"""打开指定URL"""
self.log.info(f"打开网址: {url}")
self.driver.get(url)
def find_element(self, locator):
"""查找单个元素,加入显式等待和日志"""
try:
self.log.debug(f"正在查找元素: {locator}")
element = self.wait.until(EC.presence_of_element_located(locator))
return element
except TimeoutException:
self.log.error(f"查找元素超时: {locator}")
raise
def click(self, locator):
"""点击元素"""
element = self.find_element(locator)
self.log.info(f"点击元素: {locator}")
element.click()
def input_text(self, locator, text):
"""向元素输入文本,并清空原有内容"""
element = self.find_element(locator)
element.clear()
self.log.info(f"向元素 {locator} 输入文本: {text}")
element.send_keys(text)
def get_text(self, locator):
"""获取元素的文本内容"""
element = self.find_element(locator)
text = element.text
self.log.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)
)
self.log.debug(f"元素可见: {locator}")
return True
except TimeoutException:
self.log.debug(f"元素不可见: {locator}")
return False
def take_screenshot(self, name):
"""截取屏幕截图,保存到指定目录"""
screenshot_dir = os.path.join(os.getcwd(), "screenshots")
os.makedirs(screenshot_dir, exist_ok=True) # 目录不存在则创建
file_path = os.path.join(screenshot_dir, f"{name}.png")
self.driver.save_screenshot(file_path)
self.log.info(f"截图已保存至: {file_path}")
return file_path
封装要点与避坑指南:
- 统一的等待策略 :
Selenium操作元素,90%的失败源于“元素未加载完成就进行操作”。我在find_element里强制集成了显式等待(WebDriverWait),这是最重要的稳定性保障。默认10秒,可根据网络情况调整。 - 全面的日志记录 :每个关键操作(打开页面、点击、输入)都通过
log.info记录,错误用log.error。调试时,通过日志文件就能清晰还原测试步骤,快速定位问题所在。 - 操作原子化 :
click、input_text等方法内部都调用了find_element。这样,具体的页面类(如LoginPage)只需要关心“定位符”(locator)是什么,而不用重复写查找和等待的代码。 - 截图功能集成 :把截图方法放在基类里非常方便。在用例失败时,可以在
pytest的夹具中自动调用这个方法,为问题排查留下直观证据。
3.2 驱动管理:告别手动下载和PATH配置
让每个团队成员手动下载浏览器驱动、配置系统PATH,是协作的噩梦。我们的框架必须解决这个问题。
方案:自动下载与匹配驱动 我们可以使用第三方库 webdriver-manager 。它会自动检测你电脑上的浏览器版本,并下载匹配的驱动到缓存目录,无需任何手动干预。
# 在conftest.py或专门的driver_manager.py中
from selenium import webdriver
from selenium.webdriver.chrome.service import Service as ChromeService
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
def get_driver(browser_name="chrome"):
"""根据配置创建并返回WebDriver实例"""
driver = None
if browser_name.lower() == "chrome":
# 使用webdriver-manager自动管理驱动
service = ChromeService(ChromeDriverManager().install())
options = webdriver.ChromeOptions()
# 常用选项:无头模式、忽略证书错误、禁用自动化提示
# options.add_argument('--headless') # 无头模式,不打开浏览器窗口
options.add_argument('--ignore-certificate-errors')
options.add_experimental_option("excludeSwitches", ["enable-automation"])
options.add_experimental_option('useAutomationExtension', False)
driver = webdriver.Chrome(service=service, options=options)
elif browser_name.lower() == "firefox":
service = webdriver.FirefoxService(GeckoDriverManager().install())
options = webdriver.FirefoxOptions()
driver = webdriver.Firefox(service=service, options=options)
else:
raise ValueError(f"不支持的浏览器: {browser_name}")
# 全局隐性等待(辅助,主要靠显式等待)
driver.implicitly_wait(5)
# 最大化窗口
driver.maximize_window()
return driver
实操心得 :虽然
webdriver-manager很方便,但在公司内网环境可能无法连接外网下载。这时,备选方案是将常用版本的驱动(如chromedriver)放在项目drivers/目录下,然后在代码中指定驱动路径:service = ChromeService(executable_path=‘./drivers/chromedriver’)。可以在配置文件中增加一个driver_source选项,让框架根据配置决定使用哪种方式。
3.3 配置文件管理:让框架灵活适应不同环境
测试环境( test )、预发布环境( staging )的URL、数据库连接、账号密码都不同。硬编码在代码里是绝对不可取的。我们用 configparser 库来读取 ini 格式的配置文件。
configs/config.ini 示例:
[DEFAULT]
log_level = INFO
browser = chrome
headless = false
[test]
base_url = https://test.example.com
username = test_user
password = test_pass123
[staging]
base_url = https://staging.example.com
username = staging_user
password = staging_pass456
配置读取工具 utilities/config_reader.py :
import os
import configparser
class ConfigReader:
"""配置文件读取器,单例模式确保全局唯一配置"""
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(ConfigReader, cls).__new__(cls)
cls._instance.config = configparser.ConfigParser()
config_path = os.path.join(os.path.dirname(__file__), '..', 'configs', 'config.ini')
cls._instance.config.read(config_path, encoding='utf-8')
return cls._instance
def get(self, section, option, fallback=None):
"""获取指定section和option的值"""
return self.config.get(section, option, fallback=fallback)
def getboolean(self, section, option, fallback=False):
"""获取布尔值"""
return self.config.getboolean(section, option, fallback=fallback)
# 使用示例
config = ConfigReader()
base_url = config.get('test', 'base_url')
use_headless = config.getboolean('DEFAULT', 'headless')
这样,当我们需要切换测试环境时,只需要在命令行执行用例时传入一个参数(或修改配置文件的 [DEFAULT] 节),框架就会自动使用对应环境的配置,实现了代码与环境的解耦。
4. 实操过程:从零搭建框架核心环节
理论讲得差不多了,现在我们动手,把几个最关键的部分串联起来,形成一个可运行的闭环。我会以用户登录这个最经典的场景为例,展示完整的实现流程。
4.1 第一步:环境搭建与依赖安装
任何项目开始前,先准备好环境。我强烈建议使用虚拟环境( venv )来隔离项目依赖。
# 1. 创建项目目录并进入
mkdir automation_framework && cd automation_framework
# 2. 创建虚拟环境(Python3.3+内置)
python -m venv venv
# 3. 激活虚拟环境
# Windows:
venv\Scripts\activate
# Linux/Mac:
source venv/bin/activate
# 4. 创建requirements.txt文件,并写入核心依赖
# requirements.txt 内容:
# selenium>=4.0.0
# pytest>=7.0.0
# pytest-html>=3.0.0
# allure-pytest>=2.9.0
# webdriver-manager>=3.8.0
# openpyxl>=3.0.0 # 如果需要处理Excel测试数据
# 5. 安装依赖
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
4.2 第二步:实现页面对象(以登录页面为例)
基于我们写好的 BasePage ,创建具体的页面类。这里以登录页面为例。
首先,定义定位符。我习惯用一个单独的类来管理,清晰且易于修改。
# page_objects/locators/login_page_locators.py
class LoginPageLocators:
"""登录页面所有元素的定位符"""
# 使用 (By.策略, ‘值’) 的元组形式
USERNAME_INPUT = (‘id’, ‘username’) # 假设页面用户名输入框id为‘username’
PASSWORD_INPUT = (‘id’, ‘password’)
LOGIN_BUTTON = (‘xpath’, ‘//button[@type=“submit”]’)
ERROR_MESSAGE = (‘css selector’, ‘.alert.alert-danger’)
SUCCESS_INDICATOR = (‘xpath’, ‘//h1[contains(text(), “Dashboard”)]’)
然后,实现登录页面类。
# page_objects/login_page.py
from selenium.webdriver.common.by import By
from .base_page import BasePage
from .locators.login_page_locators import LoginPageLocators
class LoginPage(BasePage):
"""登录页面操作封装"""
def __init__(self, driver):
super().__init__(driver) # 调用父类初始化,传入driver
def enter_username(self, username):
"""输入用户名"""
self.input_text(LoginPageLocators.USERNAME_INPUT, username)
def enter_password(self, password):
"""输入密码"""
self.input_text(LoginPageLocators.PASSWORD_INPUT, password)
def click_login(self):
"""点击登录按钮"""
self.click(LoginPageLocators.LOGIN_BUTTON)
def login(self, username, password):
"""登录完整流程:输入用户名、密码、点击登录"""
self.log.info(f”执行登录操作,用户名: {username}“)
self.enter_username(username)
self.enter_password(password)
self.click_login()
def get_error_message(self):
"""获取登录错误提示信息,如果存在则返回文本,否则返回空字符串"""
if self.is_element_visible(LoginPageLocators.ERROR_MESSAGE, timeout=3):
return self.get_text(LoginPageLocators.ERROR_MESSAGE)
return ”“
def is_login_successful(self):
"""通过检查登录后页面特定元素(如Dashboard标题)来判断是否登录成功"""
return self.is_element_visible(LoginPageLocators.SUCCESS_INDICATOR, timeout=10)
可以看到, LoginPage 类非常干净,它只包含这个页面特有的操作。所有与浏览器交互的底层细节(等待、查找、点击)都被 BasePage 消化了。这就是封装的魅力。
4.3 第三步:编写Pytest测试用例与夹具
现在,我们用 pytest 来编写和组织测试用例。 conftest.py 是 pytest 的本地插件文件,在这里我们可以定义全局共享的夹具( fixture )。
# test_cases/conftest.py
import pytest
from utilities.driver_manager import get_driver # 假设我们把get_driver函数放在这里
from utilities.config_reader import ConfigReader
config = ConfigReader()
@pytest.fixture(scope=“function”)
def driver():
"""为每个测试函数提供独立的driver实例"""
browser = config.get(‘DEFAULT’, ‘browser’, fallback=‘chrome’)
headless = config.getboolean(‘DEFAULT’, ‘headless’, fallback=False)
# 这里可以基于headless参数调整浏览器选项,代码略
driver_instance = get_driver(browser)
yield driver_instance # yield之前是setup,之后是teardown
# 测试函数执行完毕后,执行清理
driver_instance.quit()
print(”浏览器已关闭“)
@pytest.fixture(scope=“function”)
def login_page(driver):
"""提供一个初始化好的LoginPage实例"""
from page_objects.login_page import LoginPage
base_url = config.get(‘test’, ‘base_url’)
page = LoginPage(driver)
page.open(base_url + ‘/login’) # 拼接登录页具体路径
return page
夹具 driver 的作用域是 function ,意味着每个测试用例都会启动和关闭一次浏览器,保证用例间的隔离。 login_page 夹具则直接返回一个已经打开了登录页面的 LoginPage 对象,方便用例使用。
接下来,编写真正的测试用例。
# test_cases/test_login.py
import pytest
from utilities.config_reader import ConfigReader
config = ConfigReader()
class TestLogin:
"""登录功能测试集"""
def test_login_success(self, login_page):
"""测试正常登录流程"""
username = config.get(‘test’, ‘username’)
password = config.get(‘test’, ‘password’)
login_page.login(username, password)
# 断言:登录成功后,应该能看到成功标识(如Dashboard标题)
assert login_page.is_login_successful(), f”登录失败,未找到成功页面元素“
# 可以添加更多断言,如检查URL、页面标题等
# assert “dashboard” in login_page.driver.current_url.lower()
def test_login_failure_with_wrong_password(self, login_page):
"""测试使用错误密码登录"""
username = config.get(‘test’, ‘username’)
wrong_password = “wrong_password”
login_page.login(username, wrong_password)
# 断言:应该出现错误提示信息
error_msg = login_page.get_error_message()
assert error_msg != ”“, “预期出现错误提示,但未找到”
assert “密码错误” in error_msg or “invalid” in error_msg.lower() # 根据实际提示调整
@pytest.mark.parametrize(“username, password, expected”, [
(“”, “test_pass”, “用户名不能为空”), # 用户名空
(“test_user”, “”, “密码不能为空”), # 密码空
(“not_exist”, “test_pass”, “用户不存在”), # 用户不存在
])
def test_login_validation(self, login_page, username, password, expected):
"""参数化测试:测试各种边界和异常情况"""
login_page.login(username, password)
error_msg = login_page.get_error_message()
assert expected in error_msg, f”错误提示不符。预期包含‘{expected}’,实际为‘{error_msg}’“
这个测试文件展示了三种典型的测试用例写法:1)正常流程测试;2)异常流程测试;3)使用 @pytest.mark.parametrize 的参数化测试,用一组数据驱动多个测试场景,极大减少了代码重复。
4.4 第四步:生成漂亮的测试报告
测试跑完了,结果不能只停留在控制台。我们需要一份直观、详细的报告。 pytest-html 可以生成基础的HTML报告,而 allure 则可以生成非常美观且功能强大的报告。
使用pytest-html(简单快捷):
# 运行测试并生成html报告
pytest test_cases/test_login.py -v --html=reports/report.html --self-contained-html
--self-contained-html 参数会把CSS等资源打包进一个HTML文件,方便分享。
使用Allure(推荐,更专业):
- 首先需要安装Allure命令行工具(从官网下载或通过包管理器)。
- 运行测试时,让
pytest生成allure结果数据。pytest test_cases/test_login.py -v --alluredir=./reports/allure-results - 生成并打开
Allure报告。allure generate ./reports/allure-results -o ./reports/allure-report --clean allure open ./reports/allure-report
Allure 报告会展示用例执行时间、通过率、失败截图、步骤详情等,非常适合在团队中分享和进行问题分析。我们可以把生成报告的步骤写进一个 run_tests.py 脚本或 Makefile 里,一键执行。
5. 常见问题与排查技巧实录
框架搭起来了,用例也写了,但在实际运行中,你一定会遇到各种各样的问题。下面是我在多年实践中总结的一些高频问题和解决思路,相当于一份“急救手册”。
5.1 元素定位失败:自动化测试的头号杀手
超过70%的自动化测试失败源于元素定位问题。错误信息通常是 NoSuchElementException 或 TimeoutException 。
排查清单:
- 检查定位符是否正确 :这是第一步也是最常见的一步。用浏览器的开发者工具(F12)重新检查元素的
id、name、class或XPath是否唯一、是否稳定。警惕动态ID(包含随机字符串)。 - 检查页面是否加载完成 :虽然我们用了显式等待,但等待条件可能不对。
presence_of_element_located只要求元素存在于DOM中,但可能不可见或不可交互。如果是要点击或输入,尝试改用element_to_be_clickable或visibility_of_element_located。# 更稳健的点击操作封装 def click_safely(self, locator): element = self.wait.until(EC.element_to_be_clickable(locator)) element.click() - 检查是否有iframe :如果目标元素在
iframe内部,你必须先切换到对应的iframe,才能定位到里面的元素。# 切换到iframe iframe = self.find_element((‘tag name‘, ’iframe‘)) self.driver.switch_to.frame(iframe) # 操作iframe内的元素... # 操作完成后切回主文档 self.driver.switch_to.default_content() - 检查是否有新窗口/标签页 :点击某个链接后打开了新窗口,driver需要切换过去。
# 获取当前所有窗口句柄 original_window = self.driver.current_window_handle # 点击打开新窗口的操作... # 等待新窗口出现 WebDriverWait(self.driver, 10).until(EC.number_of_windows_to_be(2)) # 循环找到新窗口句柄并切换 for window_handle in self.driver.window_handles: if window_handle != original_window: self.driver.switch_to.window(window_handle) break - 等待时间不足 :网络慢或页面渲染慢。适当增加显式等待的超时时间,或者在操作前加入固定的
sleep(不推荐,应作为最后手段)或等待特定条件(如某个加载动画消失)。
实操心得 :对于难以定位或不稳定的元素,不要死磕
XPath或CSS。可以尝试与开发沟通,给关键测试元素添加固定的id或data-testid属性。这是提升自动化稳定性的最有效合作方式。
5.2 测试用例的独立性维护
测试用例之间应该是独立的,不依赖执行顺序。但有时会因为缓存、登录状态、浏览器数据残留导致用例间相互影响。
解决方案:
- 使用
pytest的function作用域夹具 :正如我们之前做的,每个用例都重新启动和关闭浏览器,这是最彻底的隔离,但代价是执行时间变长。 - 在用例开始前清理状态 :对于不能每个用例都重启浏览器的场景(如性能测试),可以在用例的
setup阶段(或使用@pytest.fixture(autouse=True))执行清理操作,如清除cookies、localStorage。@pytest.fixture(autouse=True) def clear_browser_data(self, driver): """每个测试自动执行:清理cookies和local storage""" driver.delete_all_cookies() driver.execute_script(“window.localStorage.clear();”) yield - 使用不同的测试账号 :对于需要登录的测试,准备一套独立的测试账号池,避免多个用例同时操作同一个账号的数据导致冲突。
5.3 测试数据的管理与参数化
测试数据(如用户名、商品ID)硬编码在用例里是坏习惯。管理测试数据,我推荐两种方式:
- JSON/YAML文件 :适合结构简单、层次清晰的数据。
// test_data/login_data.json { “valid_credentials”: { “username”: “standard_user”, “password”: “secret_sauce” }, “invalid_credentials”: [ {“username”: “”, “password”: “secret_sauce”, “error”: “Username is required”}, {“username”: “locked_user”, “password”: “secret_sauce”, “error”: “Sorry, this user has been locked out.”} ] } - Excel/CSV文件 :适合测试人员维护,尤其是大量、表格化的数据。可以使用
openpyxl或pandas库读取。 -
@pytest.mark.parametrize装饰器 :对于直接在代码里定义的小规模参数化数据,这是最方便的方式,如前文示例。
最佳实践是将静态的、大量的测试数据放在外部文件(JSON/Excel)中,在 conftest.py 里读取并转换为 pytest 可用的参数化格式,或者通过自定义夹具提供给用例。
5.4 如何在CI/CD流水线中运行(无头模式)
在Jenkins、GitLab CI等持续集成环境中,通常没有图形界面。这时需要以“无头模式”运行浏览器。
修改 get_driver 函数,增加无头模式选项:
def get_driver(browser_name=“chrome”, headless=False):
if browser_name.lower() == “chrome”:
options = webdriver.ChromeOptions()
if headless:
options.add_argument(‘--headless=new’) # Chrome较新版本推荐使用new
options.add_argument(‘--no-sandbox’) # Linux CI环境常需此参数
options.add_argument(‘--disable-dev-shm-usage’) # 解决共享内存问题
options.add_argument(‘--disable-gpu’) # 可选
options.add_argument(‘--window-size=1920,1080’) # 设置无头模式下的窗口大小
# ... 其他选项
driver = webdriver.Chrome(service=service, options=options)
# ... firefox 类似
在CI的配置文件中,通过环境变量或配置文件将 headless 设置为 True 即可。
5.5 失败用例自动截图与日志追踪
测试失败时,光有错误堆栈是不够的。自动截图和详尽的日志是快速定位问题的关键。
我们可以在 pytest 的夹具中,捕获测试失败的事件,并自动调用我们 BasePage 里的截图方法。
# test_cases/conftest.py
import pytest
from utilities.config_reader import ConfigReader
config = ConfigReader()
@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""Hook函数,用于在测试执行后获取结果并截图"""
outcome = yield
report = outcome.get_result()
# 只关注测试用例(不包括setup/teardown)的失败情况
if report.when == “call” and report.failed:
# 从测试用例中获取driver对象(假设driver夹具名为‘driver’)
driver_fixture = item.funcargs.get(‘driver’)
if driver_fixture:
# 生成唯一的截图文件名
screenshot_name = f”{item.name}_{report.when}”
try:
# 调用driver的截图方法,这里需要你的page对象或driver有截图方法
# 假设我们有一个全局可用的截图工具函数
from utilities.screenshot_helper import take_screenshot
take_screenshot(driver_fixture, screenshot_name)
# 或者,如果测试用例使用了page对象,可以这样:
# page = item.funcargs.get(‘login_page’)
# if page:
# page.take_screenshot(screenshot_name)
print(f”失败截图已保存: {screenshot_name}“)
except Exception as e:
print(f”截图失败: {e}“)
同时,确保我们的日志配置( utilities/logger.py )将不同级别的日志输出到文件,这样结合 Allure 报告,就能形成一个从“测试步骤日志”到“最终错误截图”的完整问题追溯链。
搭建一个“简单”的自动化测试框架,远不止是把几行 Selenium 代码包装起来。它涉及到项目结构设计、代码封装、配置管理、报告生成和持续集成等一系列工程化实践。这个过程中,最大的挑战往往不是技术本身,而是如何让框架足够健壮以应对千变万化的Web页面,以及如何让它足够简单以被团队所有成员接受和使用。我分享的这个框架雏形,已经包含了应对这些挑战的核心思路和关键实现。你可以以此为基础,根据自己项目的实际需求,添加更多功能,比如数据库校验、API调用混合测试、分布式测试支持等。记住,框架是为人服务的,切忌过度设计,解决实际问题的框架,才是好框架。
更多推荐



所有评论(0)