Selenium Pytest 集成 #
Pytest 简介 #
为什么选择 Pytest #
text
┌─────────────────────────────────────────────────────────────┐
│ Pytest 优势 │
├─────────────────────────────────────────────────────────────┤
│ │
│ ✅ 简洁的语法 - 无需继承类,直接使用函数 │
│ │
│ ✅ 强大的 fixture - 灵活的测试依赖管理 │
│ │
│ ✅ 参数化测试 - 轻松实现数据驱动测试 │
│ │
│ ✅ 丰富的插件 - 报告、并行执行、重试等 │
│ │
│ ✅ 详细的断言 - 失败时显示详细信息 │
│ │
│ ✅ 兼容性好 - 支持unittest测试用例 │
│ │
└─────────────────────────────────────────────────────────────┘
安装 Pytest #
bash
# 安装 pytest
pip install pytest
# 安装常用插件
pip install pytest-html pytest-xdist pytest-rerunfailures allure-pytest
# 验证安装
pytest --version
项目结构 #
推荐目录结构 #
text
selenium-pytest-project/
├── config/ # 配置文件
│ ├── __init__.py
│ ├── settings.py # 配置类
│ └── config.yaml # YAML 配置
├── pages/ # 页面对象
│ ├── __init__.py
│ ├── base_page.py # 基础页面类
│ ├── login_page.py # 登录页面
│ └── home_page.py # 首页
├── tests/ # 测试用例
│ ├── __init__.py
│ ├── conftest.py # pytest 配置
│ ├── test_login.py # 登录测试
│ └── test_search.py # 搜索测试
├── utils/ # 工具类
│ ├── __init__.py
│ ├── driver_factory.py # 驱动工厂
│ └── helpers.py # 辅助函数
├── reports/ # 测试报告
├── screenshots/ # 截图目录
├── data/ # 测试数据
│ └── test_data.json
├── pytest.ini # pytest 配置文件
├── requirements.txt # 依赖文件
└── README.md
Fixture 使用 #
基础 Fixture #
python
# tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
@pytest.fixture
def driver():
"""创建 WebDriver 实例"""
options = Options()
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
driver.maximize_window()
driver.implicitly_wait(10)
yield driver
driver.quit()
# tests/test_login.py
from selenium.webdriver.common.by import By
def test_login(driver):
"""测试登录功能"""
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys("user")
driver.find_element(By.ID, "password").send_keys("pass")
driver.find_element(By.ID, "submit").click()
assert "Dashboard" in driver.title
Fixture 作用域 #
python
# tests/conftest.py
import pytest
from selenium import webdriver
@pytest.fixture(scope="function")
def driver():
"""每个测试函数创建新的浏览器实例"""
driver = webdriver.Chrome()
yield driver
driver.quit()
@pytest.fixture(scope="class")
def class_driver():
"""每个测试类创建一个浏览器实例"""
driver = webdriver.Chrome()
yield driver
driver.quit()
@pytest.fixture(scope="module")
def module_driver():
"""每个模块创建一个浏览器实例"""
driver = webdriver.Chrome()
yield driver
driver.quit()
@pytest.fixture(scope="session")
def session_driver():
"""整个测试会话使用一个浏览器实例"""
driver = webdriver.Chrome()
yield driver
driver.quit()
Fixture 参数化 #
python
# tests/conftest.py
import pytest
from selenium import webdriver
@pytest.fixture(params=["chrome", "firefox"])
def driver(request):
"""多浏览器测试"""
browser = request.param
if browser == "chrome":
driver = webdriver.Chrome()
elif browser == "firefox":
driver = webdriver.Firefox()
else:
raise ValueError(f"不支持的浏览器: {browser}")
driver.maximize_window()
yield driver
driver.quit()
# 测试会在两个浏览器上各执行一次
def test_example(driver):
driver.get("https://example.com")
assert driver.title
Fixture 依赖 #
python
# tests/conftest.py
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
@pytest.fixture
def driver():
"""基础驱动 fixture"""
driver = webdriver.Chrome()
driver.maximize_window()
yield driver
driver.quit()
@pytest.fixture
def logged_in_driver(driver):
"""已登录的驱动 fixture"""
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys("user")
driver.find_element(By.ID, "password").send_keys("pass")
driver.find_element(By.ID, "submit").click()
return driver
# tests/test_dashboard.py
def test_dashboard(logged_in_driver):
"""测试仪表板(使用已登录状态)"""
logged_in_driver.get("https://example.com/dashboard")
assert "Dashboard" in logged_in_driver.title
参数化测试 #
基本参数化 #
python
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
@pytest.fixture
def driver():
driver = webdriver.Chrome()
yield driver
driver.quit()
@pytest.mark.parametrize("username,password,expected", [
("user1", "pass1", "success"),
("user2", "pass2", "success"),
("invalid", "invalid", "error"),
])
def test_login(driver, username, password, expected):
"""参数化登录测试"""
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys(username)
driver.find_element(By.ID, "password").send_keys(password)
driver.find_element(By.ID, "submit").click()
if expected == "success":
assert "Dashboard" in driver.title
else:
assert "Error" in driver.page_source
数据文件驱动 #
python
# data/users.json
[
{"username": "user1", "password": "pass1", "expected": "success"},
{"username": "user2", "password": "pass2", "expected": "success"},
{"username": "invalid", "password": "invalid", "expected": "error"}
]
# tests/test_login.py
import json
import pytest
def load_test_data():
with open("data/users.json") as f:
return json.load(f)
@pytest.mark.parametrize("data", load_test_data())
def test_login_data_driven(driver, data):
"""使用 JSON 数据驱动测试"""
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys(data["username"])
driver.find_element(By.ID, "password").send_keys(data["password"])
driver.find_element(By.ID, "submit").click()
if data["expected"] == "success":
assert "Dashboard" in driver.title
CSV 数据驱动 #
python
# data/users.csv
username,password,expected
user1,pass1,success
user2,pass2,success
invalid,invalid,error
# tests/test_login.py
import csv
import pytest
def load_csv_data():
with open("data/users.csv") as f:
reader = csv.DictReader(f)
return list(reader)
@pytest.mark.parametrize("data", load_csv_data())
def test_login_csv(driver, data):
driver.get("https://example.com/login")
driver.find_element(By.ID, "username").send_keys(data["username"])
driver.find_element(By.ID, "password").send_keys(data["password"])
driver.find_element(By.ID, "submit").click()
if data["expected"] == "success":
assert "Dashboard" in driver.title
测试配置 #
pytest.ini 配置 #
ini
# pytest.ini
[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
addopts =
-v
--tb=short
--html=reports/report.html
--self-contained-html
-n auto
--reruns 2
markers =
smoke: 冒烟测试
regression: 回归测试
slow: 慢速测试
conftest.py 配置 #
python
# tests/conftest.py
import pytest
import os
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def pytest_addoption(parser):
"""添加命令行选项"""
parser.addoption("--browser", action="store", default="chrome")
parser.addoption("--headless", action="store_true", default=False)
parser.addoption("--base-url", action="store", default="https://example.com")
@pytest.fixture
def browser(request):
"""获取浏览器选项"""
return request.config.getoption("--browser")
@pytest.fixture
def headless(request):
"""获取无头模式选项"""
return request.config.getoption("--headless")
@pytest.fixture
def base_url(request):
"""获取基础 URL"""
return request.config.getoption("--base-url")
@pytest.fixture
def driver(browser, headless):
"""创建 WebDriver 实例"""
if browser == "chrome":
options = Options()
if headless:
options.add_argument('--headless')
driver = webdriver.Chrome(options=options)
elif browser == "firefox":
from selenium.webdriver.firefox.options import Options as FirefoxOptions
options = FirefoxOptions()
if headless:
options.add_argument('--headless')
driver = webdriver.Firefox(options=options)
else:
raise ValueError(f"不支持的浏览器: {browser}")
driver.maximize_window()
driver.implicitly_wait(10)
yield driver
driver.quit()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""测试失败时截图"""
outcome = yield
report = outcome.get_result()
if report.when == "call" and report.failed:
driver = item.funcargs.get('driver')
if driver:
screenshot_dir = "screenshots"
os.makedirs(screenshot_dir, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
screenshot_path = os.path.join(screenshot_dir, f"{item.name}_{timestamp}.png")
driver.save_screenshot(screenshot_path)
测试标记 #
使用标记 #
python
import pytest
from selenium import webdriver
@pytest.mark.smoke
def test_login_smoke(driver):
"""冒烟测试 - 登录"""
driver.get("https://example.com/login")
assert "Login" in driver.title
@pytest.mark.regression
def test_login_regression(driver):
"""回归测试 - 登录"""
driver.get("https://example.com/login")
# 详细测试...
@pytest.mark.slow
@pytest.mark.skip(reason="功能未完成")
def test_slow_feature(driver):
"""跳过的测试"""
pass
@pytest.mark.smoke
@pytest.mark.regression
def test_search(driver):
"""同时标记多个"""
pass
运行特定标记 #
bash
# 只运行冒烟测试
pytest -m smoke
# 运行冒烟测试和回归测试
pytest -m "smoke or regression"
# 运行冒烟测试但排除慢速测试
pytest -m "smoke and not slow"
# 跳过特定标记
pytest -m "not slow"
测试报告 #
HTML 报告 #
bash
# 安装插件
pip install pytest-html
# 生成报告
pytest --html=reports/report.html --self-contained-html
python
# tests/conftest.py
import pytest
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""自定义报告内容"""
outcome = yield
report = outcome.get_result()
# 添加额外信息到报告
if report.when == "call":
report.sections.append(("Extra Info", "Additional details here"))
Allure 报告 #
bash
# 安装插件
pip install allure-pytest
# 运行测试生成数据
pytest --alluredir=reports/allure-results
# 生成报告
allure serve reports/allure-results
python
import pytest
import allure
from selenium import webdriver
@allure.feature("登录功能")
@allure.story("用户登录")
class TestLogin:
@pytest.fixture(autouse=True)
def setup(self, driver):
self.driver = driver
@allure.title("正常登录测试")
@allure.severity(allure.severity_level.CRITICAL)
def test_login_success(self):
"""测试正常登录"""
with allure.step("打开登录页面"):
self.driver.get("https://example.com/login")
with allure.step("输入用户名和密码"):
self.driver.find_element(By.ID, "username").send_keys("user")
self.driver.find_element(By.ID, "password").send_keys("pass")
with allure.step("点击登录按钮"):
self.driver.find_element(By.ID, "submit").click()
with allure.step("验证登录成功"):
assert "Dashboard" in self.driver.title
# 添加截图到报告
allure.attach(
self.driver.get_screenshot_as_png(),
name="登录成功截图",
attachment_type=allure.attachment_type.PNG
)
并行执行 #
使用 pytest-xdist #
bash
# 安装插件
pip install pytest-xdist
# 自动并行(CPU 核心数)
pytest -n auto
# 指定进程数
pytest -n 4
# 按文件分发
pytest --dist=loadfile
并行测试配置 #
python
# tests/conftest.py
import pytest
from selenium import webdriver
@pytest.fixture(scope="session")
def driver_pool():
"""驱动池(session 级别)"""
drivers = []
def create_driver():
driver = webdriver.Chrome()
drivers.append(driver)
return driver
yield create_driver
for driver in drivers:
driver.quit()
@pytest.fixture
def driver(driver_pool):
"""每个测试获取独立驱动"""
driver = driver_pool()
yield driver
driver.delete_all_cookies()
测试重试 #
使用 pytest-rerunfailures #
bash
# 安装插件
pip install pytest-rerunfailures
# 失败重试 2 次
pytest --reruns 2
# 重试间隔 1 秒
pytest --reruns 2 --reruns-delay 1
标记重试 #
python
import pytest
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_flaky_feature(driver):
"""不稳定的测试,自动重试"""
driver.get("https://example.com")
# 可能失败的测试...
完整示例 #
测试框架结构 #
python
# config/settings.py
from dataclasses import dataclass
@dataclass
class Config:
base_url: str = "https://example.com"
username: str = "test_user"
password: str = "test_password"
implicit_wait: int = 10
page_load_timeout: int = 30
config = Config()
# utils/driver_factory.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
class DriverFactory:
@staticmethod
def create_driver(browser="chrome", headless=False):
if browser == "chrome":
options = Options()
if headless:
options.add_argument('--headless')
options.add_argument('--no-sandbox')
options.add_argument('--disable-dev-shm-usage')
driver = webdriver.Chrome(options=options)
elif browser == "firefox":
from selenium.webdriver.firefox.options import Options as FirefoxOptions
options = FirefoxOptions()
if headless:
options.add_argument('--headless')
driver = webdriver.Firefox(options=options)
else:
raise ValueError(f"不支持的浏览器: {browser}")
return driver
# tests/conftest.py
import pytest
from utils.driver_factory import DriverFactory
from config.settings import config
@pytest.fixture
def driver(request):
browser = request.config.getoption("--browser", default="chrome")
headless = request.config.getoption("--headless", default=False)
driver = DriverFactory.create_driver(browser, headless)
driver.maximize_window()
driver.implicitly_wait(config.implicit_wait)
yield driver
driver.quit()
# tests/test_login.py
import pytest
from selenium.webdriver.common.by import By
from config.settings import config
class TestLogin:
def test_login_success(self, driver):
"""测试登录成功"""
driver.get(f"{config.base_url}/login")
driver.find_element(By.ID, "username").send_keys(config.username)
driver.find_element(By.ID, "password").send_keys(config.password)
driver.find_element(By.ID, "submit").click()
assert "Dashboard" in driver.title
def test_login_invalid_password(self, driver):
"""测试密码错误"""
driver.get(f"{config.base_url}/login")
driver.find_element(By.ID, "username").send_keys(config.username)
driver.find_element(By.ID, "password").send_keys("wrong_password")
driver.find_element(By.ID, "submit").click()
assert "Error" in driver.page_source
下一步 #
掌握了 Pytest 集成后,接下来学习 页面对象模式 了解如何设计可维护的测试代码!
最后更新:2026-03-28