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