Python自动化测试框架实战:从pytest选型到CI/CD集成
1. 项目概述:为什么我们需要一个自动化测试框架?
如果你是一名开发或者测试工程师,每天还在手动点点点,或者写着一堆零散、重复的测试脚本,那感觉一定糟透了。我经历过那个阶段,一个功能改动,测试用例要手动跑上半天,效率低下不说,还容易遗漏。后来,我接触到了Python自动化测试框架,它彻底改变了我的工作流。简单来说,一个成熟的自动化测试框架,就像给你的测试工作搭建了一条标准化的“流水线”。它规定了脚本怎么写、数据怎么管、用例怎么跑、报告怎么出,让自动化测试从“手工作坊”变成了“现代化工厂”。
这不仅仅是写几行 unittest 或 pytest 代码那么简单。一个完整的框架,需要解决测试数据管理、环境隔离、用例依赖、失败重试、报告生成、持续集成对接等一系列工程化问题。市面上有很多选择,比如 pytest 、 unittest 、 Robot Framework ,还有结合 Selenium 、 Appium 、 Playwright 做UI自动化的,或者用 requests 、 httpx 做接口自动化的。但万变不离其宗,其核心目标都是提升测试效率、保证测试质量、降低维护成本。今天,我就结合自己踩过的坑和积累的经验,带你从零开始,彻底搞懂如何构建和运用一个属于你自己的、健壮的Python自动化测试框架。
2. 核心框架选型与设计哲学
2.1 主流框架横向对比:pytest为何成为首选?
在Python的测试世界里, unittest 是标准库自带的“元老”,而 pytest 则是后来居上的“社区宠儿”。对于新手,可能会从 unittest 入门,因为它“开箱即用”。但一旦你的测试规模扩大, pytest 的优势就无可比拟。
为什么我强烈推荐pytest?
- 更简洁的语法 :
pytest不需要你继承任何类,一个以test_开头的函数就是一个测试用例。断言直接用assert,直观易懂。相比之下,unittest需要继承TestCase,使用self.assertEqual()等方法,略显繁琐。 - 强大的Fixture机制 :这是
pytest的灵魂。Fixture可以理解为测试的“夹具”,用于提供测试所需的数据、环境或资源(如数据库连接、临时文件、浏览器实例)。它完美解决了测试前置(setup)和后置(teardown)的代码复用问题,并且支持作用域(函数、类、模块、会话级),管理起来非常灵活。 - 丰富的插件生态 :这是
pytest生态繁荣的关键。你需要生成漂亮的HTML报告?有pytest-html。需要控制用例执行顺序?有pytest-ordering。需要多进程并行跑测试?有pytest-xdist。需要生成覆盖率报告?有pytest-cov。几乎你能想到的任何增强功能,都有对应的插件。 - 优秀的失败信息展示 :当断言失败时,
pytest会给出非常详细的差异对比,帮你快速定位问题,而unittest的信息往往不够直观。
Robot Framework 则是另一个思路,它是一个关键字驱动的自动化框架,更适合测试人员(尤其是非开发背景)通过编写接近自然语言的用例来执行自动化。它功能强大,但灵活性不如 pytest ,更适合在特定领域(如RPA)或团队技能结构偏测试的情况下使用。
对于绝大多数以开发或测试开发角色为主的团队, pytest + 各类功能库(Selenium/Requests/Appium) 的组合,提供了最佳的生产力和灵活性平衡点。我们的框架也将以 pytest 为核心进行构建。
2.2 框架设计核心思想:模块化与可配置性
在动手写代码之前,先要想清楚框架的骨架。一个好的框架设计,应该遵循“高内聚、低耦合”的原则。我通常会将项目结构规划为以下几个核心目录:
project_root/
├── config/ # 配置文件目录
│ ├── __init__.py
│ ├── config.yaml # 或 config.ini, config.py
│ └── env_config.py # 环境配置(测试/预发/生产)
├── test_cases/ # 测试用例目录
│ ├── __init__.py
│ ├── test_api/ # 接口测试用例
│ ├── test_ui/ # UI测试用例
│ └── test_mobile/ # 移动端测试用例
├── common/ # 公共模块目录
│ ├── __init__.py
│ ├── base_page.py # UI页面对象基类
│ ├── api_client.py # 接口请求封装类
│ └── logger.py # 日志模块封装
├── fixtures/ # pytest fixtures目录
│ ├── __init__.py
│ └── conftest.py # 全局fixture定义
├── test_data/ # 测试数据目录
│ ├── data.yaml
│ ├── sql/
│ └── json/
├── reports/ # 测试报告目录(通常.gitignore)
│ └── html/
├── logs/ # 运行日志目录(通常.gitignore)
└── requirements.txt # 项目依赖
设计要点解析:
- config/ : 将环境变量、数据库连接串、账号密码、URL等所有可变配置外置。通过读取不同的配置文件(如
config_test.yaml,config_prod.yaml)来切换测试环境,避免将硬编码写入脚本。 - common/ : 封装所有可复用的代码。例如,将Selenium的常用操作(查找元素、点击、输入)封装在
BasePage类中,所有页面对象继承它。对HTTP请求的封装(添加通用头、处理鉴权、解析响应)放在APIClient类中。这样,当底层工具库升级或需要统一修改逻辑时,只需改动这一个地方。 - fixtures/conftest.py : 这是
pytest的魔力所在。在这里定义的fixture可以被整个项目或所在目录的测试用例自动发现和使用。我们将浏览器初始化、数据库连接、登录态获取等重量级操作定义为fixture。 - test_data/ : 测试数据与脚本分离是基本原则。可以将用例数据放在YAML、JSON或Excel中,甚至连接测试数据库获取。这样,修改测试数据无需改动代码。
实操心得 :在项目初期就严格遵循这个目录结构,哪怕用例很少。这能强制你形成良好的代码组织习惯,当项目膨胀到几百个用例时,你会感谢当初的自己。
3. 环境搭建与核心工具链配置
3.1 Python环境与依赖管理:虚拟环境是必须的
第一步,永远是为你的自动化项目创建一个独立的虚拟环境。这能避免不同项目间的包版本冲突。
# 使用venv创建虚拟环境(Python3.3+内置)
python -m venv venv
# 激活虚拟环境
# Windows:
venv\Scripts\activate
# Linux/Mac:
source venv/bin/activate
# 激活后,命令行提示符前会出现 (venv) 标识
接下来,将项目依赖写入 requirements.txt 文件。一个典型的自动化测试项目依赖可能如下:
# requirements.txt
pytest>=7.0.0
pytest-html>=3.0.0
pytest-xdist>=3.0.0
pytest-rerunfailures>=10.0
pytest-ordering>=0.6
requests>=2.28.0
selenium>=4.0.0
webdriver-manager>=3.8.0 # 自动管理浏览器驱动
allure-pytest>=2.9.0 # 生成Allure报告
PyYAML>=6.0 # 读写YAML配置文件
openpyxl>=3.0.0 # 处理Excel测试数据
pymysql>=1.0.0 # 数据库操作
loguru>=0.6.0 # 更优雅的日志记录
使用pip一键安装:
pip install -r requirements.txt
为什么选择这些库?
webdriver-manager:强烈推荐!它自动下载和匹配对应版本的浏览器驱动(ChromeDriver, GeckoDriver),彻底解决了“驱动版本不匹配”这个经典难题。loguru:比标准库的logging配置简单太多,输出格式美观,是提升日志体验的利器。allure-pytest:生成的Allure报告比pytest-html的报告更加美观、交互性更强,是向团队展示测试结果的最佳选择之一。
3.2 IDE配置与效率提升:VSCode与PyCharm之争
对于编辑器,VSCode和PyCharm是两大主流。我的建议是:
- 新手或轻量级项目 :选VSCode。它轻快、免费,通过安装Python、Pytest、YAML等插件,完全可以胜任自动化测试开发。其内置的终端和源码管理也很方便。
- 大型企业级项目或深度Python开发 :选PyCharm Professional。它对Django、Flask等Web框架、数据库工具、科学计算的支持更专业,调试功能也更强大。社区版对自动化测试也足够用。
关键插件/配置:
- VSCode :安装官方
Python扩展、Pytest扩展。在设置中配置python.testing.pytestEnabled为true,这样可以在侧边栏直接发现和运行测试用例。 - PyCharm :在
Settings -> Tools -> Python Integrated Tools中,将Default test runner设置为pytest。 - 通用配置 :在项目根目录创建
.vscode/settings.json或.idea目录来统一团队编码风格(如使用black或autopep8作为格式化工具)。
4. 测试用例编写与组织实战
4.1 接口自动化测试:从零封装一个健壮的APIClient
接口测试是自动化测试的基石,速度快、稳定性高。我们不应该在每个用例里直接写 requests.get() ,而应该进行封装。
首先,在 common/api_client.py 中创建一个通用的客户端:
import requests
from loguru import logger
from typing import Any, Dict, Optional
class APIClient:
def __init__(self, base_url: str):
self.base_url = base_url.rstrip('/')
self.session = requests.Session()
# 可以在这里设置默认请求头,如Content-Type, User-Agent
self.session.headers.update({
'Content-Type': 'application/json; charset=utf-8',
'User-Agent': 'MyAutomationFramework/1.0'
})
def _request(self, method: str, endpoint: str, **kwargs) -> requests.Response:
"""发送请求的核心方法"""
url = f"{self.base_url}{endpoint}"
# 记录请求日志(敏感信息如密码需脱敏)
logger.info(f"Request: {method.upper()} {url}")
if kwargs.get('json'):
logger.debug(f"Request Body: {kwargs['json']}")
if kwargs.get('params'):
logger.debug(f"Request Params: {kwargs['params']}")
try:
resp = self.session.request(method, url, **kwargs)
resp.raise_for_status() # 如果状态码不是2xx,抛出HTTPError异常
except requests.exceptions.RequestException as e:
logger.error(f"Request failed: {e}")
raise
# 记录响应日志
logger.info(f"Response Status: {resp.status_code}")
logger.debug(f"Response Body: {resp.text[:500]}") # 只记录前500字符,避免日志过长
return resp
# 定义便捷方法
def get(self, endpoint: str, params: Optional[Dict] = None, **kwargs):
return self._request('GET', endpoint, params=params, **kwargs)
def post(self, endpoint: str, json: Optional[Dict] = None, **kwargs):
return self._request('POST', endpoint, json=json, **kwargs)
def put(self, endpoint: str, json: Optional[Dict] = None, **kwargs):
return self._request('PUT', endpoint, json=json, **kwargs)
def delete(self, endpoint: str, **kwargs):
return self._request('DELETE', endpoint, **kwargs)
# 可以添加更多方法,如上传文件、form-data等
然后,在 fixtures/conftest.py 中定义一个全局fixture来提供这个客户端:
import pytest
from common.api_client import APIClient
from config import config # 假设config模块能读取到BASE_URL
@pytest.fixture(scope="session")
def api_client():
"""提供全局的API客户端实例"""
client = APIClient(base_url=config.BASE_URL)
# 这里可以进行全局的登录操作,将token存入session headers
# login_resp = client.post("/login", json={"username": "...", "password": "..."})
# client.session.headers['Authorization'] = f"Bearer {login_resp.json()['token']}"
yield client
# 测试结束后可以做一些清理,如退出登录
# client.post("/logout")
最后,在 test_cases/test_api/test_user.py 中编写用例:
class TestUserAPI:
def test_get_user_info(self, api_client):
"""测试获取用户信息"""
resp = api_client.get("/api/v1/user/123")
assert resp.status_code == 200
data = resp.json()
assert data['id'] == 123
assert 'username' in data
# 更复杂的断言可以使用jsonpath或schema验证库
def test_create_user(self, api_client):
"""测试创建用户"""
new_user = {"username": "test_user", "email": "test@example.com"}
resp = api_client.post("/api/v1/users", json=new_user)
assert resp.status_code == 201
created_user = resp.json()
assert created_user['username'] == new_user['username']
# 通常创建后需要清理,可以将删除操作放在fixture或用例teardown中
封装的好处 :所有与HTTP相关的细节(异常处理、日志记录、默认头)都被隐藏了。用例编写者只需关心业务逻辑:发送什么数据,期望什么结果。当需要统一添加签名、加密或修改重试策略时,只需改动 APIClient 类。
4.2 UI自动化测试:Page Object模式深度实践
UI自动化(Web)最怕的就是元素定位表达式散落在各个测试用例中,页面一改,所有用例都要崩溃。Page Object (PO) 模式是解决这个问题的标准答案。
PO模式核心思想 :将一个页面(或一个页面片段)封装成一个类,页面的元素定位器是这个类的属性,页面的操作(点击、输入)是这个类的方法。测试用例只与这些页面对象交互,不直接操作Selenium。
第一步,创建页面基类 common/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 loguru import logger
class BasePage:
def __init__(self, driver):
self.driver = driver
self.wait = WebDriverWait(driver, timeout=10, poll_frequency=0.5)
def find_element(self, locator: tuple):
"""查找单个元素,带显式等待"""
try:
element = self.wait.until(EC.presence_of_element_located(locator))
return element
except TimeoutException:
logger.error(f"Element not found: {locator}")
raise
def click(self, locator: tuple):
"""点击元素"""
element = self.find_element(locator)
try:
element.click()
except StaleElementReferenceException:
# 元素可能已过时,重新查找
element = self.find_element(locator)
element.click()
logger.info(f"Clicked element: {locator}")
def input_text(self, locator: tuple, text: str):
"""向输入框输入文本"""
element = self.find_element(locator)
element.clear()
element.send_keys(text)
logger.info(f"Input '{text}' into element: {locator}")
def get_text(self, locator: tuple) -> str:
"""获取元素文本"""
element = self.find_element(locator)
return element.text
# 可以添加更多通用方法:截图、滚动、切换窗口/iframe等
第二步,创建具体的页面对象,例如 pages/login_page.py :
from common.base_page import BasePage
class LoginPage(BasePage):
# 元素定位器 (By.ID, "id_value") 或 (By.XPATH, "xpath_expression")
USERNAME_INPUT = ("id", "username")
PASSWORD_INPUT = ("id", "password")
LOGIN_BUTTON = ("xpath", "//button[@type='submit']")
ERROR_MSG = ("css selector", ".alert-error")
def __init__(self, driver):
super().__init__(driver)
def login(self, username: str, password: str):
"""登录操作"""
self.input_text(self.USERNAME_INPUT, username)
self.input_text(self.PASSWORD_INPUT, password)
self.click(self.LOGIN_BUTTON)
def get_error_message(self) -> str:
"""获取错误提示信息"""
return self.get_text(self.ERROR_MSG)
第三步,在 fixtures/conftest.py 中定义浏览器fixture:
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from pages.login_page import LoginPage
@pytest.fixture(scope="function") # 每个测试函数一个浏览器实例,保证隔离
def browser():
options = webdriver.ChromeOptions()
options.add_argument('--headless') # 无头模式,不打开GUI,适合CI环境
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
options.add_argument('--disable-gpu')
service = Service(ChromeDriverManager().install())
driver = webdriver.Chrome(service=service, options=options)
driver.implicitly_wait(5) # 隐式等待(备用)
driver.maximize_window()
yield driver
driver.quit() # 测试结束后关闭浏览器
@pytest.fixture
def login_page(browser):
"""提供登录页面实例"""
browser.get("https://your-app.com/login") # 从配置读取URL
return LoginPage(browser)
第四步,编写UI测试用例 test_cases/test_ui/test_login.py :
def test_login_success(login_page):
"""测试登录成功"""
login_page.login("correct_user", "correct_password")
# 断言:登录后应跳转到首页,可以通过URL或首页特定元素判断
# 例如:assert login_page.driver.current_url == "https://your-app.com/dashboard"
# 或者:assert login_page.find_element(HomePage.WELCOME_MSG).is_displayed()
def test_login_failure(login_page):
"""测试登录失败"""
login_page.login("wrong_user", "wrong_password")
error_text = login_page.get_error_message()
assert "invalid" in error_text.lower() # 断言错误信息包含特定关键词
PO模式的优势 :
- 高可维护性 :页面元素定位符只存在于页面对象类中。UI改动时,只需更新对应的页面类。
- 高可读性 :测试用例读起来像自然语言(
login_page.login(...)),清晰地表达了业务意图。 - 低冗余 :公共操作(如等待、点击)封装在基类,避免重复代码。
注意事项 :UI自动化天生比接口测试更脆弱(网络、渲染速度、弹窗干扰)。除了使用显式等待,关键操作后可以加入短暂的
time.sleep(1)作为稳定锚点,并配合失败重试机制(pytest-rerunfailures)。
4.3 测试数据驱动:让用例与数据分离
数据驱动测试(DDT)是提高用例复用性的关键。同一个测试逻辑,可以用多组不同的输入输出数据来验证。 pytest 的 @pytest.mark.parametrize 装饰器是原生支持。
示例:用多组数据测试登录功能
import pytest
# 将测试数据定义在用例文件内(适用于数据量小的情况)
test_login_data = [
("admin", "admin123", True, "登录成功"),
("admin", "wrong_pwd", False, "密码错误"),
("", "admin123", False, "用户名为空"),
("admin", "", False, "密码为空"),
]
@pytest.mark.parametrize("username, password, expected_success, expected_msg", test_login_data)
def test_login_with_data(login_page, username, password, expected_success, expected_msg):
login_page.login(username, password)
if expected_success:
# 验证登录成功
assert login_page.driver.current_url != "https://your-app.com/login"
else:
# 验证登录失败,并提示信息包含expected_msg
actual_msg = login_page.get_error_message()
assert expected_msg in actual_msg
更优实践:从外部文件读取数据 当数据量很大或需要非技术人员维护时,应将数据放在外部文件(YAML/JSON/Excel)中。
- 创建YAML数据文件
test_data/login_data.yaml:
- username: "admin"
password: "admin123"
expected_success: true
expected_msg_part: "dashboard"
- username: "test_user"
password: "wrong"
expected_success: false
expected_msg_part: "invalid"
- 编写数据加载工具
common/data_loader.py:
import yaml
import json
import os
def load_yaml_data(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return yaml.safe_load(f)
def load_json_data(file_path):
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
# 可以根据文件后缀自动选择加载器
- 在conftest中定义数据fixture :
import pytest
from common.data_loader import load_yaml_data
@pytest.fixture(params=load_yaml_data("test_data/login_data.yaml"))
def login_case_data(request):
"""参数化fixture,每一条数据都会生成一个测试用例"""
return request.param
- 在用例中使用数据fixture :
def test_login_with_external_data(login_page, login_case_data):
data = login_case_data
login_page.login(data['username'], data['password'])
# ... 使用data['expected_success']等进行断言
这种方式将测试逻辑与数据彻底解耦,新增测试场景只需在YAML文件中加一条数据即可。
5. 高级特性与工程化集成
5.1 测试报告与结果可视化:生成专业报告
测试执行完了,一份清晰、直观的报告至关重要。 pytest-html 和 allure-pytest 是两个主流选择。
使用pytest-html生成报告: 安装后,运行测试时添加参数即可。
pytest --html=reports/report.html --self-contained-html
--self-contained-html 参数会将CSS和JS内联到HTML中,生成单个文件,便于分享。报告会包含测试概述、通过/失败/跳过的用例列表、失败用例的详细日志和截图(需配合钩子函数实现截图)。
使用Allure生成更强大的报告: Allure报告更加现代和交互式。
- 运行测试,生成原始数据:
pytest --alluredir=./allure-results - 使用Allure命令行工具生成HTML报告:
allure generate ./allure-results -o ./reports/html --clean allure open ./reports/html # 在浏览器中打开报告 - 为了在报告中附加截图或日志,可以在
conftest.py中编写钩子函数:import allure from selenium import webdriver @pytest.hookimpl(tryfirst=True, hookwrapper=True) def pytest_runtest_makereport(item, call): """在测试执行过程中制作报告,用于失败截图""" outcome = yield rep = outcome.get_result() if rep.when == "call" and rep.failed: # 判断当前fixture是否有browser(WebDriver实例) for name, fixtureinfo in item._fixtureinfo.name2fixturedefs.items(): if name == "browser": browser = item.funcargs["browser"] if isinstance(browser, webdriver.Remote): # 截图并附加到Allure报告 screenshot = browser.get_screenshot_as_png() allure.attach(screenshot, name="失败截图", attachment_type=allure.attachment_type.PNG) # 也可以附加页面源代码 # page_source = browser.page_source # allure.attach(page_source, name="页面源码", attachment_type=allure.attachment_type.TEXT) break
Allure报告支持分类、标签、趋势图、环境信息等,是向团队和管理层展示测试成果的利器。
5.2 并发执行与测试调度:大幅缩短反馈时间
当你有成百上千个测试用例时,顺序执行会非常耗时。 pytest-xdist 插件可以实现并行测试。
# 使用2个worker并行执行
pytest -n 2
# 自动检测CPU核心数
pytest -n auto
并行执行的注意事项:
- 测试隔离 :并行用例必须相互独立,不能有共享状态(如操作同一个测试账号、修改同一行数据库记录)。这需要通过测试数据隔离(每个进程用独立的数据)或使用事务回滚来保证。
- 资源竞争 :UI测试并行时,需要确保每个浏览器实例有独立的端口或用户数据目录,避免冲突。
webdriver-manager能很好地处理驱动问题。 - Fixture作用域 :小心使用
scope="session"或scope="module"的fixture,它们在所有worker中可能只初始化一次,可能导致意外共享。对于数据库连接这类资源,可以考虑使用scope="function"或专门的并行处理策略。
5.3 持续集成/持续部署(CI/CD)集成
自动化测试只有集成到CI/CD流水线中,才能最大化其价值。这里以GitHub Actions为例,展示一个简单的集成配置。
在项目根目录创建 .github/workflows/python-test.yml :
name: Python Automated Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.9", "3.10"] # 测试多个Python版本
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Install Chrome and ChromeDriver (for UI tests)
run: |
sudo apt-get update
sudo apt-get install -y wget unzip
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | sudo tee /etc/apt/sources.list.d/google-chrome.list
sudo apt-get update
sudo apt-get install -y google-chrome-stable
# 使用webdriver-manager,此步可省略手动安装驱动
- name: Run tests with pytest
run: |
# 运行测试,生成Allure结果和HTML报告
pytest --alluredir=allure-results --html=reports/report.html --self-contained-html -v
- name: Upload test report (HTML)
uses: actions/upload-artifact@v3
if: always() # 即使测试失败也上传报告
with:
name: pytest-html-report-${{ matrix.python-version }}
path: reports/report.html
- name: Upload Allure results
uses: actions/upload-artifact@v3
if: always()
with:
name: allure-results-${{ matrix.python-version }}
path: allure-results/
这个工作流会在每次推送到主分支或发起Pull Request时,在Ubuntu环境下安装依赖、浏览器,并运行所有测试,最后将测试报告作为制品保存,供下载查看。
6. 常见问题排查与性能优化
6.1 稳定性提升:处理元素定位与异步加载
UI自动化不稳定,十有八九出在“等待”上。
-
抛弃
time.sleep,拥抱显式等待 :time.sleep(固定时间)是糟糕的实践,它要么等太久(浪费时间),要么等不够(导致失败)。始终使用WebDriverWait配合expected_conditions。# 坏例子 time.sleep(5) element = driver.find_element(...) # 好例子 from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By wait = WebDriverWait(driver, 10) element = wait.until(EC.presence_of_element_located((By.ID, "dynamic-element"))) # 或者等待元素可点击 button = wait.until(EC.element_to_be_clickable((By.XPATH, "//button[text()='Submit']"))) -
使用更稳定的定位策略 :优先级:ID > Name > CSS Selector > XPath。尽量避免使用绝对XPath(以
/开头),它极度脆弱。使用相对XPath或CSS Selector。# 脆弱 driver.find_element(By.XPATH, "/html/body/div[3]/div[2]/form/input[1]") # 相对好一些 driver.find_element(By.XPATH, "//input[@id='username']") # 更好 (如果元素有ID) driver.find_element(By.ID, "username") # 或使用CSS Selector driver.find_element(By.CSS_SELECTOR, "input.form-control[name='username']") -
处理Shadow DOM/iframe :现代前端框架可能使用Shadow DOM,某些元素可能在iframe内。需要先切换到正确的上下文。
# 切换iframe iframe = driver.find_element(By.TAG_NAME, "iframe") driver.switch_to.frame(iframe) # 操作iframe内的元素... driver.switch_to.default_content() # 操作完切回来 # 访问Shadow DOM (较新版本的Selenium支持) shadow_host = driver.find_element(By.CSS_SELECTOR, "#shadow-host") shadow_root = shadow_host.shadow_root inner_element = shadow_root.find_element(By.CSS_SELECTOR, ".inner-element")
6.2 测试用例依赖与执行顺序管理
理想情况下,每个测试用例都应该是独立的。但有时难免存在依赖,比如“创建订单”用例必须在“登录”之后。 pytest 默认不保证用例顺序,但提供了控制方法。
-
使用
pytest-ordering插件控制顺序 (谨慎使用):import pytest @pytest.mark.run(order=1) def test_login(): pass @pytest.mark.run(order=2) def test_create_order(): pass # 依赖test_login创建的登录态更好的做法是将依赖部分(如登录)提取为
fixture,并设置合适的scope(如scope="class"或scope="module"),让依赖它的用例自动执行前置操作。 -
使用Fixture依赖 :这是更
pytest的方式。import pytest @pytest.fixture def login_session(api_client): """登录并返回session""" resp = api_client.post("/login", json={"user": "test", "pwd": "123"}) token = resp.json()["token"] api_client.session.headers.update({"Authorization": f"Bearer {token}"}) return api_client def test_create_order(login_session): # login_session fixture会先执行 resp = login_session.post("/order", json={...}) assert resp.status_code == 201
6.3 性能优化与资源清理
-
Fixture作用域选择 :合理设置fixture的作用域可以大幅提升执行速度。
scope="session":整个测试会话只执行一次(如创建数据库连接池)。scope="module":每个测试文件执行一次。scope="class":每个测试类执行一次。scope="function":默认值,每个测试函数执行一次(如每个UI测试用独立的浏览器)。 对于耗时的操作(如启动浏览器、初始化大数据),尽量使用更大范围的作用域。
-
善用
yield进行清理 :Fixture支持yield语法,yield之前的代码是setup,之后的代码是teardown,无论测试成功与否都会执行。@pytest.fixture(scope="function") def temp_file(): # Setup: 创建临时文件 file_path = "/tmp/test_data.txt" with open(file_path, 'w') as f: f.write("test data") yield file_path # 将文件路径提供给测试用例使用 # Teardown: 测试结束后清理文件 import os if os.path.exists(file_path): os.remove(file_path) -
数据库测试数据清理 :对于修改了数据库的测试,一定要在测试后清理,避免影响后续测试。可以在fixture中使用数据库事务,测试后回滚;或者使用专门的测试数据库,每次测试前用脚本重置。
构建一个健壮的Python自动化测试框架,远不止是学会 pytest 和 Selenium 的API。它更像是在搭建一个微型的产品,需要考虑架构设计、代码规范、可维护性、执行效率和团队协作。从简单的脚本开始,逐步引入Page Object、数据驱动、Fixture、配置文件、日志和报告,最终集成到CI/CD流水线中,这是一个不断迭代和优化的过程。最关键的还是开始动手去做,在真实的项目中遇到问题、解决问题,你的框架才会越来越贴合实际需求,真正成为提升研发效能的利器。
更多推荐

所有评论(0)