自动化测试领域迎来大地震,Selenium 4.0官方首发完整评测报告

自动化测试领域迎来大地震,Selenium 4.0官方首发完整评测报告

自动化测试领域迎来大地震,Selenium 4.0官方首发完整评测报告


为什么 Selenium 值得关注

在现代软件开发中,Web 应用的自动化测试已经从“可选项”变成了“必选项”。根据最新统计数据,超过 80% 的开发团队已经在 CI/CD 流程中集成了自动化测试。而提到 Web 自动化测试工具,有一个名字几乎无人不知、无人不晓——Selenium

Selenium 不仅仅是一个工具,它更是一个完整的生态系统。从最初的 JavaScript 注入式测试工具,发展到今天支持多语言、多平台、多浏览器的工业级测试框架,Selenium 已经成为 Web 自动化领域的事实标准。

选择 Selenium 的核心理由:

  • 跨浏览器支持: Chrome、Firefox、Safari、Edge、IE,一套代码搞定所有主流浏览器
  • 多语言绑定: Python、Java、C#、Ruby、JavaScript,总有一款适合你和你的团队
  • 生态成熟: 15 年以上的社区积累,数以万计的教程和解决方案
  • 集成友好: 无缝对接 Jenkins、Docker、CI/CD 流水线
  • 免费开源: 零成本商用,没有任何授权费用

如果你正在为团队寻找一个稳定、可靠、社区活跃的 Web 自动化测试方案,Selenium 依然是 2024 年的首选。接下来,我将带你从零开始,系统掌握 Selenium 的使用方法。


环境搭建

系统要求

在开始之前,确保你的系统满足以下要求:

  • 操作系统: Windows 10/11、macOS 10.14+、Linux (Ubuntu 18.04+)
  • Python 版本: 3.7 或更高版本
  • 内存: 最低 4GB,推荐 8GB 以上
  • 硬盘空间: 至少 2GB 可用空间

安装 Python(如果还没有)

访问 Python 官方网站下载并安装 Python 3.7 或更高版本。安装时记得勾选 “Add Python to PATH” 选项。

安装完成后,打开终端验证:

python --version
pip --version

安装 Selenium

使用 pip 安装 Selenium 是最简单的方式:

pip install selenium

如果你需要特定版本:

pip install selenium==4.15.0

安装 WebDriver

Selenium 4 需要与浏览器对应的 WebDriver。以下是各浏览器的驱动下载指南:

ChromeDriver

  1. 打开 Chrome,访问 chrome://settings/help 查看版本号
  2. 访问 https://googlechromelabs.github.io/chrome-for-testing/ 下载对应版本
  3. 解压后将可执行文件放入系统 PATH 目录

GeckoDriver (Firefox)

  1. 访问 https://github.com/mozilla/geckodriver/releases
  2. 下载对应平台版本
  3. 解压并放入系统 PATH 目录

EdgeDriver

  1. 打开 Edge,访问 edge://settings/help 查看版本号
  2. 访问 https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ 下载对应版本

验证安装

创建一个 Python 文件 check_installation.py

from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options

# 配置 Chrome 选项
options = Options()
options.add_argument("--headless")  # 无头模式,不显示浏览器窗口
options.add_argument("--no-sandbox")
options.add_argument("--disable-dev-shm-usage")

# 指定 WebDriver 路径(如果不在 PATH 中)
service = Service("/path/to/chromedriver")

try:
    driver = webdriver.Chrome(service=service, options=options)
    print("Selenium 安装成功!")
    print(f"浏览器版本: {driver.capabilities['browserVersion']}")
    print(f"ChromeDriver 版本: {driver.capabilities['chrome']['chromedriverVersion'].split(' ')[0]}")
    driver.quit()
except Exception as e:
    print(f"安装验证失败: {e}")

运行验证脚本:

python check_installation.py

如果看到 “Selenium 安装成功!”,恭喜你,环境已经准备就绪。


核心功能详解

Selenium 架构概览

Selenium 由多个组件构成,理解它们的关系对深入学习至关重要:

Selenium IDE
    ↓
Selenium Grid
    ↓
WebDriver APIs (多语言)
    ↓
Browser Drivers (ChromeDriver, GeckoDriver, etc.)
    ↓
Browsers (Chrome, Firefox, Safari, Edge)

WebDriver 核心概念

WebDriver 是 Selenium 的核心,它提供了一套面向对象的 API 来控制浏览器行为。理解以下概念是掌握 Selenium 的基础:

1. WebElement(网页元素)

WebElement 代表 HTML 文档中的任何一个元素,如按钮、输入框、链接等。所有对元素的交互都通过 WebElement 对象完成。

2. By 定位策略

Selenium 支持多种元素定位策略:

定位策略 说明 示例
ID 通过元素 ID 属性定位 By.ID, "username"
NAME 通过 name 属性定位 By.NAME, "email"
CLASS_NAME 通过 class 属性定位 By.CLASS_NAME, "btn-primary"
TAG_NAME 通过 HTML 标签名定位 By.TAG_NAME, "input"
LINK_TEXT 通过完整链接文本定位 By.LINK_TEXT, "点击这里"
PARTIAL_LINK_TEXT 通过部分链接文本定位 By.PARTIAL_LINK_TEXT, "点击"
XPATH 通过 XPath 表达式定位 By.XPATH, "//div[@class='container']"
CSS_SELECTOR 通过 CSS 选择器定位 By.CSS_SELECTOR, ".main > .content"

3. Expected Conditions (显式等待)

显式等待是 Selenium 4 中最重要的改进之一,它允许你指定等待某个条件成立再继续执行。

Selenium 4 新特性

Selenium 4 带来了多项重要升级:

1. 相对定位器 (Relative Locators)

Selenium 4 引入了直观的相对定位方式:

from selenium.webdriver.support.relative_locator import locate_with

# 定位 "提交" 按钮,它在 "用户名" 输入框的右侧
submit_button = driver.find_element(
    locate_with(By.TAG_NAME, "button").to_right_of(username_input)
)

# 定位 "取消" 按钮,它在某个元素的下方
cancel_button = driver.find_element(
    locate_with(By.TAG_NAME, "button").below(submit_button)
)

2. 窗口和标签页管理改进

# 获取当前窗口句柄
main_window = driver.current_window_handle

# 打开新标签页(Selenium 4 新方法)
driver.switch_to.new_window('tab')

# 打开新窗口
driver.switch_to.new_window('window')

# 关闭窗口并切换回主窗口
driver.close()
driver.switch_to.window(main_window)

3. 打印页面为 PDF

# 将网页打印为 PDF(仅支持 Chrome)
from selenium.webdriver.chromium.printing import PrintOptions

print_options = PrintOptions()
print_options.page_ranges = ['1-2']

pdf_bytes = driver.print_page(print_options)

with open('page.pdf', 'wb') as f:
    f.write(pdf_bytes)

4. 动作链改进

# Selenium 4 的动作链更加直观
from selenium.webdriver.common.action_chains import ActionChains

actions = ActionChains(driver)
actions.move_to_element(menu).pause(1).click(hidden_submenu)
actions.perform()

实战教程:构建完整的自动化测试脚本

实战项目:百度搜索自动化

让我们通过一个完整的实战项目来学习 Selenium。项目目标:自动化执行百度搜索功能,并验证搜索结果。

第一步:创建项目结构

selenium_tutorial/
├── config/
│   └── settings.py
├── pages/
│   └── baidu_page.py
├── tests/
│   └── test_baidu_search.py
├── utils/
│   └── driver_setup.py
├── requirements.txt
└── run_tests.py

第二步:配置管理

创建 config/settings.py

"""
配置文件 - 集中管理测试环境配置
"""

class Config:
    # 浏览器配置
    BROWSER_NAME = "chrome"
    HEADLESS_MODE = False
    WINDOW_SIZE = (1920, 1080)

    # 隐式等待配置(秒)
    IMPLICIT_WAIT = 10

    # 显式等待配置(秒)
    EXPLICIT_WAIT = 20

    # 页面加载超时(秒)
    PAGE_LOAD_TIMEOUT = 30

    # 目标网站
    BASE_URL = "https://www.baidu.com"

    # 截图保存路径
    SCREENSHOT_DIR = "screenshots"

    # 日志配置
    LOG_LEVEL = "INFO"
    LOG_FILE = "test_logs.log"

第三步:封装 WebDriver 初始化

创建 utils/driver_setup.py

"""
WebDriver 初始化工具类
提供统一的浏览器驱动创建和销毁逻辑
"""

import os
import logging
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.firefox.options import Options as FirefoxOptions
from selenium.webdriver.edge.options import Options as EdgeOptions
from config.settings import Config

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)


def create_driver(browser_name="chrome", headless=False):
    """
    创建 WebDriver 实例

    Args:
        browser_name: 浏览器名称 (chrome, firefox, edge)
        headless: 是否使用无头模式

    Returns:
        WebDriver 实例
    """
    browser_name = browser_name.lower()

    if browser_name == "chrome":
        return _create_chrome_driver(headless)
    elif browser_name == "firefox":
        return _create_firefox_driver(headless)
    elif browser_name == "edge":
        return _create_edge_driver(headless)
    else:
        raise ValueError(f"不支持的浏览器: {browser_name}")


def _create_chrome_driver(headless):
    """创建 Chrome WebDriver"""
    options = Options()

    if headless:
        options.add_argument("--headless=new")

    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--disable-gpu")
    options.add_argument(f"--window-size={Config.WINDOW_SIZE[0]},{Config.WINDOW_SIZE[1]}")
    options.add_argument("--disable-blink-features=AutomationControlled")
    options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")

    # 禁用自动化信息栏
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_experimental_option("useAutomationExtension", False)

    # 创建服务对象
    service = Service()

    # 创建驱动实例
    driver = webdriver.Chrome(service=service, options=options)

    # 执行 CDP 命令禁用 webdriver
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": """
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined
            })
        """
    })

    # 配置等待时间
    driver.implicitly_wait(Config.IMPLICIT_WAIT)
    driver.set_page_load_timeout(Config.PAGE_LOAD_TIMEOUT)

    logger.info("Chrome WebDriver 创建成功")
    return driver


def _create_firefox_driver(headless):
    """创建 Firefox WebDriver"""
    options = FirefoxOptions()

    if headless:
        options.add_argument("--headless")

    options.add_argument("--width=1920")
    options.add_argument("--height=1080")

    service = Service()
    driver = webdriver.Firefox(service=service, options=options)
    driver.implicitly_wait(Config.IMPLICIT_WAIT)

    logger.info("Firefox WebDriver 创建成功")
    return driver


def _create_edge_driver(headless):
    """创建 Edge WebDriver"""
    options = EdgeOptions()

    if headless:
        options.add_argument("--headless=new")

    options.add_argument("--disable-gpu")
    options.add_argument(f"--window-size={Config.WINDOW_SIZE[0]},{Config.WINDOW_SIZE[1]}")

    service = Service()
    driver = webdriver.Edge(service=service, options=options)
    driver.implicitly_wait(Config.IMPLICIT_WAIT)

    logger.info("Edge WebDriver 创建成功")
    return driver


def quit_driver(driver):
    """
    安全关闭 WebDriver

    Args:
        driver: WebDriver 实例
    """
    if driver:
        try:
            driver.quit()
            logger.info("WebDriver 已成功关闭")
        except Exception as e:
            logger.error(f"关闭 WebDriver 时出错: {e}")

第四步:封装页面对象

创建 pages/baidu_page.py

"""
百度首页页面对象类
封装百度搜索页面的所有元素定位和交互方法
"""

import logging
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
from config.settings import Config

logger = logging.getLogger(__name__)


class BaiduPage:
    """
    百度首页页面对象

    遵循 Page Object 设计模式,将页面元素和操作方法封装在一起
    """

    # ==== 页面元素定位器 ====

    # 搜索输入框
    SEARCH_INPUT = (By.ID, "kw")

    # 搜索按钮
    SEARCH_BUTTON = (By.ID, "su")

    # 搜索结果列表(首个结果)
    FIRST_RESULT = (By.XPATH, "//div[@id='content_left']//h3/a")

    # 搜索结果标题列表
    RESULT_TITLES = (By.XPATH, "//div[@id='content_left']//h3/a")

    # 搜索结果数量提示
    RESULT_STATS = (By.CLASS_NAME, "nums_text")

    # 设置按钮
    SETTINGS_BUTTON = (By.LINK_TEXT, "设置")

    # 搜索设置链接
    SEARCH_SETTINGS = (By.LINK_TEXT, "搜索设置")

    # 搜索结果页面标题
    RESULT_PAGE_TITLE = (By.TAG_NAME, "title")

    def __init__(self, driver):
        """
        初始化页面对象

        Args:
            driver: WebDriver 实例
        """
        self.driver = driver
        self.wait = WebDriverWait(driver, Config.EXPLICIT_WAIT)
        self.timeout = Config.EXPLICIT_WAIT

    def open(self):
        """打开百度首页"""
        logger.info(f"正在打开: {Config.BASE_URL}")
        self.driver.get(Config.BASE_URL)
        self._verify_page_loaded()
        return self

    def _verify_page_loaded(self):
        """验证页面加载成功"""
        try:
            self.wait.until(EC.presence_of_element_located(self.SEARCH_INPUT))
            logger.info("百度首页加载成功")
        except TimeoutException:
            logger.error("百度首页加载超时")
            raise

    def search(self, keyword):
        """
        执行搜索操作

        Args:
            keyword: 搜索关键词

        Returns:
            SearchResultPage 实例
        """
        logger.info(f"正在搜索: {keyword}")

        # 清空输入框并输入关键词
        input_element = self.wait_for_element_visible(self.SEARCH_INPUT)
        input_element.clear()
        input_element.send_keys(keyword)

        # 点击搜索按钮
        search_button = self.wait_for_element_clickable(self.SEARCH_BUTTON)
        search_button.click()

        logger.info("搜索完成")
        return SearchResultPage(self.driver, keyword)

    def wait_for_element_visible(self, locator, timeout=None):
        """
        等待元素可见

        Args:
            locator: 元素定位器
            timeout: 超时时间(秒)

        Returns:
            WebElement
        """
        timeout = timeout or self.timeout
        try:
            element = WebDriverWait(self.driver, timeout).until(
                EC.visibility_of_element_located(locator)
            )
            return element
        except TimeoutException:
            logger.error(f"元素不可见: {locator}")
            raise

    def wait_for_element_clickable(self, locator, timeout=None):
        """
        等待元素可点击

        Args:
            locator: 元素定位器
            timeout: 超时时间(秒)

        Returns:
            WebElement
        """
        timeout = timeout or self.timeout
        try:
            element = WebDriverWait(self.driver, timeout).until(
                EC.element_to_be_clickable(locator)
            )
            return element
        except TimeoutException:
            logger.error(f"元素不可点击: {locator}")
            raise

    def get_page_title(self):
        """获取页面标题"""
        return self.driver.title

    def take_screenshot(self, filename):
        """
        截取页面截图

        Args:
            filename: 保存文件名
        """
        import os
        os.makedirs(Config.SCREENSHOT_DIR, exist_ok=True)
        filepath = os.path.join(Config.SCREENSHOT_DIR, filename)
        self.driver.save_screenshot(filepath)
        logger.info(f"截图已保存: {filepath}")


class SearchResultPage:
    """
    百度搜索结果页面对象
    """

    # ==== 搜索结果页元素 ====
    RESULT_CONTAINER = (By.ID, "content_left")
    RESULT_COUNT = (By.CLASS_NAME, "nums_text")
    NO_RESULT_MESSAGE = (By.XPATH, "//div[@class='nnr_null']")

    def __init__(self, driver, keyword):
        self.driver = driver
        self.keyword = keyword
        self.wait = WebDriverWait(driver, Config.EXPLICIT_WAIT)

    def wait_for_results(self):
        """等待搜索结果加载"""
        try:
            self.wait.until(
                EC.presence_of_element_located(self.RESULT_CONTAINER)
            )
            logger.info("搜索结果加载完成")
        except TimeoutException:
            logger.warning("未检测到搜索结果容器")

    def get_result_count(self):
        """
        获取搜索结果数量

        Returns:
            整数结果数量,如果无法获取返回 -1
        """
        try:
            stats_element = self.wait.until(
                EC.presence_of_element_located(self.RESULT_COUNT)
            )
            stats_text = stats_element.text
            logger.info(f"搜索统计信息: {stats_text}")

            # 解析结果数量,例如 "百度为您找到相关结果约1,234,567个"
            import re
            match = re.search(r'约([\d,]+)', stats_text)
            if match:
                count_str = match.group(1).replace(',', '')
                return int(count_str)
            return -1
        except Exception as e:
            logger.error(f"获取结果数量失败: {e}")
            return -1

    def get_first_result_title(self):
        """
        获取首个搜索结果的标题

        Returns:
            标题文本
        """
        try:
            first_result = self.wait.until(
                EC.presence_of_element_located(
                    (By.XPATH, "//div[@id='content_left']//h3/a")
                )
            )
            title = first_result.text
            logger.info(f"首个结果标题: {title}")
            return title
        except TimeoutException:
            logger.error("未找到搜索结果")
            return None

    def click_first_result(self):
        """
        点击首个搜索结果

        Returns:
            目标页面 URL
        """
        try:
            first_link = self.wait.until(
                EC.element_to_be_clickable(
                    (By.XPATH, "//div[@id='content_left']//h3/a")
                )
            )
            url = first_link.get_attribute("href")
            first_link.click()
            logger.info(f"点击了首个结果,跳转到: {url}")
            return url
        except TimeoutException:
            logger.error("无法点击首个搜索结果")
            raise

    def get_all_result_titles(self):
        """
        获取所有搜索结果的标题列表

        Returns:
            标题文本列表
        """
        try:
            results = self.wait.until(
                EC.presence_of_all_elements_located(
                    (By.XPATH, "//div[@id='content_left']//h3/a")
                )
            )
            titles = [result.text for result in results]
            logger.info(f"获取到 {len(titles)} 个结果标题")
            return titles
        except TimeoutException:
            logger.error("无法获取结果标题列表")
            return []

    def is_keyword_in_results(self, keyword):
        """
        验证关键词是否出现在搜索结果中

        Args:
            keyword: 要检查的关键词

        Returns:
            True 如果关键词出现在结果中
        """
        titles = self.get_all_result_titles()
        for title in titles:
            if keyword.lower() in title.lower():
                return True
        return False

第五步:编写测试用例

创建 tests/test_baidu_search.py

"""
百度搜索自动化测试用例
包含多个测试场景,覆盖搜索功能的各个方面
"""

import pytest
import logging
from pages.baidu_page import BaiduPage, SearchResultPage
from utils.driver_setup import create_driver, quit_driver
from config.settings import Config

logger = logging.getLogger(__name__)


@pytest.fixture(scope="module")
def driver():
    """
    测试夹具:创建和销毁 WebDriver
    scope="module" 表示在整个测试模块中复用同一个驱动实例
    """
    logger.info("初始化 WebDriver")
    driver = create_driver(
        browser_name=Config.BROWSER_NAME,
        headless=Config.HEADLESS_MODE
    )
    yield driver
    logger.info("清理 WebDriver")
    quit_driver(driver)


@pytest.fixture
def baidu_page(driver):
    """打开百度首页的夹具"""
    page = BaiduPage(driver)
    page.open()
    return page


class TestBaiduSearch:
    """百度搜索功能测试类"""

    def test_basic_search(self, baidu_page):
        """
        测试用例:基本搜索功能

        验证点:
        1. 能够成功打开百度首页
        2. 搜索框可正常输入
        3. 点击搜索后结果页面加载成功
        """
        keyword = "Selenium 自动化测试"

        # 执行搜索
        result_page = baidu_page.search(keyword)

        # 验证结果页面加载
        result_page.wait_for_results()

        # 验证搜索结果数量
        count = result_page.get_result_count()
        assert count > 0, f"期望有搜索结果,实际得到 {count}"
        logger.info(f"✓ 基本搜索测试通过,找到 {count} 个结果")

    def test_search_result_contains_keyword(self, baidu_page):
        """
        测试用例:验证搜索结果包含关键词

        验证点:
        1. 搜索结果的标题中应包含搜索关键词
        """
        keyword = "Python"

        result_page = baidu_page.search(keyword)
        result_page.wait_for_results()

        # 检查首个结果
        first_title = result_page.get_first_result_title()
        assert first_title is not None, "首个结果标题不应为空"

        # 验证关键词出现在结果中(可能出现在描述中)
        count = result_page.get_result_count()
        assert count > 0, "应该有搜索结果"

        logger.info(f"✓ 关键词验证测试通过,搜索 '{keyword}' 找到 {count} 个结果")

    def test_empty_search(self, baidu_page):
        """
        测试用例:空关键词搜索

        验证点:
        1. 空关键词不应该导致程序崩溃
        2. 应该给出合理的提示或结果
        """
        result_page = baidu_page.search("")

        # 等待页面响应
        import time
        time.sleep(2)

        # 获取页面标题,验证没有错误
        title = baidu_page.get_page_title()
        assert "百度" in title, f"页面标题异常: {title}"

        logger.info("✓ 空搜索测试通过,页面正常响应")

    def test_special_characters_search(self, baidu_page):
        """
        测试用例:特殊字符搜索

        验证点:
        1. 特殊字符不应导致程序崩溃
        2. 百度应该能正确处理特殊字符
        """
        special_keywords = [
            "Python + Java",  # 加号
            "测试 @ 自动化",  # 邮箱符号
            "C++ 编程",       # 加号
            "100% 正确",     # 百分号
        ]

        for keyword in special_keywords:
            result_page = baidu_page.search(keyword)
            result_page.wait_for_results()
            count = result_page.get_result_count()
            logger.info(f"关键词 '{keyword}' 找到 {count} 个结果")

        logger.info("✓ 特殊字符测试通过")

    def test_navigation_from_result(self, baidu_page):
        """
        测试用例:从搜索结果导航

        验证点:
        1. 点击搜索结果后能正确跳转到目标页面
        2. 跳转后 URL 与点击链接一致
        """
        keyword = "GitHub"

        result_page = baidu_page.search(keyword)
        result_page.wait_for_results()

        # 保存当前窗口句柄
        original_window = baidu_page.driver.current_window_handle

        # 点击首个结果
        clicked_url = result_page.click_first_result()

        # 等待新窗口打开
        import time
        time.sleep(3)

        # 切换到新窗口
        windows = baidu_page.driver.window_handles
        for window in windows:
            if window != original_window:
                baidu_page.driver.switch_to.window(window)
                break

        # 验证新页面加载
        current_url = baidu_page.driver.current_url
        logger.info(f"成功跳转到: {current_url}")

        # 关闭新窗口并切换回原窗口
        baidu_page.driver.close()
        baidu_page.driver.switch_to.window(original_window)

        logger.info("✓ 导航测试通过")


class TestBaiduPageElements:
    """百度页面元素交互测试"""

    def test_search_input_operations(self, baidu_page):
        """
        测试用例:搜索框操作

        验证点:
        1. 能够清空搜索框
        2. 能够输入文本
        3. 能够获取输入框内容
        """
        from selenium.webdriver.support.ui import WebDriverWait
        from selenium.webdriver.support import expected_conditions as EC

        # 定位搜索框
        search_input = baidu_page.wait_for_element_visible(
            BaiduPage.SEARCH_INPUT
        )

        # 清空输入框
        search_input.clear()

        # 输入文本
        test_text = "自动化测试"
        search_input.send_keys(test_text)

        # 验证输入内容
        actual_text = search_input.get_attribute("value")
        assert actual_text == test_text, \
            f"期望输入 '{test_text}',实际得到 '{actual_text}'"

        # 验证清空功能
        search_input.clear()
        actual_text = search_input.get_attribute("value")
        assert actual_text == "", f"清空后输入框应为空,实际为 '{actual_text}'"

        logger.info("✓ 搜索框操作测试通过")

    def test_screenshot_capture(self, baidu_page):
        """
        测试用例:截图功能

        验证点:
        1. 能够在任意时刻截取页面
        2. 截图文件正确保存
        """
        import os

        # 截取首页
        baidu_page.take_screenshot("baidu_home.png")

        # 验证文件存在
        screenshot_path = os.path.join(
            Config.SCREENSHOT_DIR, 
            "baidu_home.png"
        )
        assert os.path.exists(screenshot_path), \
            f"截图文件不存在: {screenshot_path}"

        logger.info("✓ 截图功能测试通过")


if __name__ == "__main__":
    # 直接运行此文件进行测试
    pytest.main([__file__, "-v", "--tb=short"])

第六步:运行测试

创建 run_tests.py

"""
测试运行器
提供便捷的测试执行入口
"""

import sys
import subprocess
import logging

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)


def run_tests(test_path=None, browser="chrome", headless=False, verbose=True):
    """
    运行测试

    Args:
        test_path: 测试文件或目录路径,None 表示运行所有测试
        browser: 使用的浏览器
        headless: 是否使用无头模式
        verbose: 是否输出详细日志
    """
    cmd = ["pytest"]

    # 添加测试路径
    if test_path:
        cmd.append(test_path)
    else:
        cmd.append("tests/")

    # 添加 pytest 选项
    cmd.extend(["--tb=short", "-p", "no:warnings"])

    if verbose:
        cmd.append("-v")

    # 添加浏览器参数
    if browser:
        cmd.extend(["--browser", browser])

    if headless:
        cmd.append("--headless")

    logging.info(f"执行命令: {' '.join(cmd)}")

    result = subprocess.run(cmd)
    return result.returncode


if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Selenium 测试运行器")
    parser.add_argument(
        "-t", "--test", 
        help="测试文件路径",
        default=None
    )
    parser.add_argument(
        "-b", "--browser",
        help="浏览器类型 (chrome/firefox/edge)",
        default="chrome"
    )
    parser.add_argument(
        "--headless",
        help="使用无头模式",
        action="store_true"
    )

    args = parser.parse_args()

    exit_code = run_tests(
        test_path=args.test,
        browser=args.browser,
        headless=args.headless
    )

    sys.exit(exit_code)

运行测试:

python run_tests.py --browser chrome --headless

或者直接使用 pytest:

pytest tests/test_baidu_search.py -v

常见使用场景与进阶应用

场景一:表单自动化填写

表单填写是 Web 自动化中最常见的场景之一。以下是一个完整的注册表单填写示例:

"""
表单填写自动化示例
演示如何处理各种类型的表单元素
"""

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import Select, WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.chrome.service import Service

# 初始化驱动
service = Service()
driver = webdriver.Chrome(service=service)

try:
    # ==== 打开表单页面 ====
    driver.get("https://example.com/register")
    wait = WebDriverWait(driver, 20)

    # ==== 文本输入框 ====
    # 普通文本输入
    username = wait.until(
        EC.element_to_be_clickable((By.NAME, "username"))
    )
    username.clear()
    username.send_keys("test_user_2024")

    # 邮箱输入
    email = driver.find_element(By.NAME, "email")
    email.clear()
    email.send_keys("test@example.com")

    # 密码输入
    password = driver.find_element(By.NAME, "password")
    password.clear()
    password.send_keys("SecureP@ssw0rd!")

    # ==== 下拉选择框 ====
    # 方法一:使用 Select 类
    country_select = Select(driver.find_element(By.NAME, "country"))
    country_select.select_by_visible_text("China")

    # 方法二:根据值选择
    # country_select.select_by_value("CN")

    # 方法三:根据索引选择(第二个选项)
    # country_select.select_by_index(1)

    # ==== 单选按钮 ====
    # 选择性别为"男"
    gender_male = driver.find_element(
        By.CSS_SELECTOR, "input[name='gender'][value='male']"
    )
    gender_male.click()

    # ==== 复选框 ====
    # 勾选同意条款
    terms_checkbox = driver.find_element(By.NAME, "agree_terms")
    if not terms_checkbox.is_selected():
        terms_checkbox.click()

    # 勾选订阅新闻
    newsletter_checkbox = driver.find_element(By.NAME, "subscribe_newsletter")
    if not newsletter_checkbox.is_selected():
        newsletter_checkbox.click()

    # ==== 日期选择器 ====
    # 对于原生日期选择器
    birthdate = driver.find_element(By.NAME, "birthdate")
    birthdate.clear()
    birthdate.send_keys("1990-01-15")

    # ==== 文件上传 ====
    # 使用 file input 上传
    avatar_upload = driver.find_element(By.NAME, "avatar")
    avatar_upload.send_keys("/path/to/avatar.jpg")

    # ==== 文本域(多行文本) ====
    bio = driver.find_element(By.NAME, "bio")
    bio.clear()
    bio.send_keys("我是一名软件测试工程师,\n热爱自动化测试技术,\n专注于提升测试效率。")

    # ==== 使用键盘快捷键 ====
    # 在输入框中按 Tab 键跳转到下一个字段
    bio.send_keys(Keys.TAB)

    # 全选并复制(Ctrl+A, Ctrl+C)
    username.send_keys(Keys.CONTROL + "a")
    username.send_keys(Keys.CONTROL + "c")

    # ==== 悬停与下拉菜单 ====
    # 如果有悬停触发的菜单
    from selenium.webdriver.common.action_chains import ActionChains

    actions = ActionChains(driver)

    # 悬停到用户菜单
    user_menu = driver.find_element(By.CLASS_NAME, "user-menu")
    actions.move_to_element(user_menu).perform()

    # 点击出现的子菜单项
    settings_link = wait.until(
        EC.element_to_be_clickable((By.LINK_TEXT, "Settings"))
    )
    settings_link.click()

    # ==== 提交表单 ====
    # 方法一:点击提交按钮
    submit_button = driver.find_element(
        By.CSS_SELECTOR, "button[type='submit']"
    )
    submit_button.click()

    # 方法二:按回车键提交
    # password.send_keys(Keys.RETURN)

    # ==== 等待表单提交完成 ====
    wait.until(EC.url_changes(driver.current_url))

    # 验证提交成功
    success_message = wait.until(
        EC.visibility_of_element_located(
            (By.CLASS_NAME, "alert-success")
        )
    )
    print(f"表单提交成功: {success_message.text}")

finally:
    driver.quit()

场景二:滚动与动态内容加载

很多现代网页使用无限滚动加载内容,Selenium 提供了多种处理方式:

"""
滚动与动态内容加载处理
演示多种滚动策略
"""

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.keys import Keys
import time

def scroll_by_pixels(driver, pixels):
    """
    按像素滚动

    Args:
        driver: WebDriver 实例
        pixels: 滚动像素数(正数向下,负数向上)
    """
    driver.execute_script(f"window.scrollBy(0, {pixels});")
    time.sleep(0.5)


def scroll_to_element(driver, element):
    """
    滚动到指定元素可见

    Args:
        driver: WebDriver 实例
        element: WebElement 对象
    """
    driver.execute_script("arguments[0].scrollIntoView({behavior: 'smooth', block: 'center'});", element)
    time.sleep(0.5)


def scroll_to_page_bottom(driver):
    """滚动到页面底部"""
    driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    time.sleep(1)


def scroll_to_page_top(driver):
    """滚动到页面顶部"""
    driver.execute_script("window.scrollTo(0, 0);")
    time.sleep(0.5)


def infinite_scroll_with_condition(driver, max_scrolls=10, wait_time=2):
    """
    无限滚动直到满足条件或达到最大次数

    Args:
        driver: WebDriver 实例
        max_scrolls: 最大滚动次数
        wait_time: 每次滚动后等待时间(秒)

    Returns:
        最终页面高度
    """
    last_height = driver.execute_script("return document.body.scrollHeight")

    for i in range(max_scrolls):
        # 滚动到页面底部
        driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

        # 等待加载
        time.sleep(wait_time)

        # 计算新高度
        new_height = driver.execute_script("return document.body.scrollHeight")

        # 如果高度没有变化,说明已经到底了
        if new_height == last_height:
            print(f"滚动 {i+1} 次后页面高度未变化,可能已加载全部内容")
            break

        last_height = new_height
        print(f"第 {i+1} 次滚动,当前高度: {new_height}")

    return last_height


def lazy_load_images(driver):
    """
    懒加载图片处理 - 触发所有图片加载

    通过滚动触发布局中的懒加载图片
    """
    # 获取所有图片元素
    images = driver.find_elements(By.TAG_NAME, "img")

    for img in images:
        try:
            # 滚动到图片位置
            driver.execute_script("arguments[0].scrollIntoView({block: 'center'});", img)
            time.sleep(0.3)

            # 获取图片属性
            src = img.get_attribute("src")
            data_src = img.get_attribute("data-src")

            # 如果有 data-src,加载真实图片
            if data_src:
                driver.execute_script(
                    "arguments[0].src = arguments[1];", 
                    img, 
                    data_src
                )
        except Exception as e:
            print(f"处理图片时出错: {e}")

    time.sleep(1)


# ==== 实际应用示例 ====
def scrape_infinite_scroll_page(driver, target_count=100):
    """
    抓取无限滚动页面的数据

    Args:
        driver: WebDriver 实例
        target_count: 目标抓取数量

    Returns:
        抓取到的数据列表
    """
    driver.get("https://example.com/infinite-scroll")
    wait = WebDriverWait(driver, 20)

    # 等待初始内容加载
    wait.until(EC.presence_of_element_located((By.CLASS_NAME, "item")))

    scraped_items = []
    scroll_count = 0

    while len(scraped_items) < target_count:
        # 获取当前可见的所有项目
        items = driver.find_elements(By.CLASS_NAME, "item")

        for item in items:
            if item not in scraped_items:
                scraped_items.append(item)
                # 可以在这里提取具体数据
                # title = item.find_element(By.CLASS_NAME, "title").text

        if len(scraped_items) >= target_count:
            break

        # 滚动加载更多
        driver.execute_script(
            "window.scrollTo(0, document.body.scrollHeight);"
        )
        scroll_count += 1

        # 等待新内容加载
        time.sleep(2)

        # 检查是否还有更多内容
        new_items = driver.find_elements(By.CLASS_NAME, "item")
        if len(new_items) == len(items):
            print(f"滚动 {scroll_count} 次后无新内容加载,停止")
            break

        print(f"滚动 {scroll_count} 次,已获取 {len(scraped_items)} 个项目")

    return scraped_items

场景三:处理弹窗与对话框

Web 应用中有各种类型的弹窗,Selenium 提供了统一的处理方式:

"""
弹窗与对话框处理
包括 Alert、Confirm、Prompt,以及自定义模态框
"""

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.alert import Alert

def handle_alert(driver, accept=True, dismiss_text=None):
    """
    处理 JavaScript Alert 弹窗

    Args:
        driver: WebDriver 实例
        accept: True 接受,False 拒绝
        dismiss_text: 如果是 Prompt,输入的文本

    Returns:
        Alert 文本内容
    """
    wait = WebDriverWait(driver, 10)
    wait.until(EC.alert_is_present())

    alert = Alert(driver)
    alert_text = alert.text

    if dismiss_text:
        alert.send_keys(dismiss_text)

    if accept:
        alert.accept()
    else:
        alert.dismiss()

    return alert_text


def handle_confirm_dialog(driver, accept=True):
    """处理确认对话框"""
    return handle_alert(driver, accept=accept)


def handle_prompt_dialog(driver, text, accept=True):
    """
    处理提示输入对话框

    Args:
        driver: WebDriver 实例
        text: 要输入的文本
        accept: True 确认,False 取消
    """
    return handle_alert(driver, accept=accept, dismiss_text=text)


def handle_modal_dialog(driver, timeout=10):
    """
    处理自定义模态对话框(Bootstrap、Material UI 等)

    Args:
        driver: WebDriver 实例
        timeout: 超时时间

    Returns:
        模态框元素
    """
    wait = WebDriverWait(driver, timeout)

    # 等待模态框出现
    modal = wait.until(
        EC.visibility_of_element_located(
            (By.CSS_SELECTOR, ".modal.show, .modal-dialog")
        )
    )

    return modal


def handle_custom_modal(driver):
    """
    处理自定义模态框的完整流程

    包括:打开模态框、填写表单、点击按钮、关闭模态框
    """
    wait = WebDriverWait(driver, 10)

    # 1. 点击触发按钮
    trigger_button = driver.find_element(By.ID, "open-modal-btn")
    trigger_button.click()

    # 2. 等待模态框出现
    modal = wait.until(
        EC.visibility_of_element_located((By.CLASS_NAME, "modal-content"))
    )

    # 3. 在模态框中操作
    # 填写表单
    name_input = modal.find_element(By.NAME, "name")
    name_input.send_keys("张三")

    email_input = modal.find_element(By.NAME, "email")
    email_input.send_keys("zhangsan@example.com")

    # 4. 点击确认按钮
    confirm_btn = modal.find_element(
        By.CSS_SELECTOR, 
        ".modal-footer .btn-primary"
    )
    confirm_btn.click()

    # 5. 等待模态框关闭
    wait.until(EC.invisibility_of_element_located((By.CLASS_NAME, "modal")))

    # 6. 验证结果
    success_toast = wait.until(
        EC.visibility_of_element_located((By.CLASS_NAME, "toast-success"))
    )
    print(f"操作成功: {success_toast.text}")


def handle_iframe(driver):
    """
    处理 iframe 内的元素

    iframe 通常用于嵌入第三方内容(如广告、视频、聊天窗口等)
    """
    wait = WebDriverWait(driver, 10)

    # ==== 切换到 iframe ====
    # 方法一:通过 frame 名称或 ID
    # driver.switch_to.frame("frame_name")

    # 方法二:通过 WebElement
    # iframe_element = driver.find_element(By.TAG_NAME, "iframe")
    # driver.switch_to.frame(iframe_element)

    # 方法三:通过索引(第二个 iframe)
    # driver.switch_to.frame(1)

    # ==== 在 iframe 中操作 ====
    # 等待 iframe 加载
    wait.until(EC.frame_to_be_available_and_switch_to_it(
        (By.ID, "content-frame")
    ))

    # 在 iframe 中查找元素
    iframe_content = driver.find_element(By.TAG_NAME, "body")
    print(f"IFrame 内容: {iframe_content.text[:100]}...")

    # ==== 操作完成后切换回主文档 ====
    driver.switch_to.default_content()

    # 或者切换到父框架
    # driver.switch_to.parent_frame()

    print("已切回主文档")


def handle_multiple_windows(driver):
    """
    处理多窗口场景

    典型场景:点击链接在新窗口打开
    """
    wait = WebDriverWait(driver, 10)

    # 保存当前窗口句柄
    main_window = driver.current_window_handle

    # 点击在新窗口打开的链接
    link = driver.find_element(By.LINK_TEXT, "Open in New Window")
    link.click()

    # 等待新窗口出现
    wait.until(EC.number_of_windows_to_be(2))

    # 获取所有窗口句柄
    all_windows = driver.window_handles

    # 切换到新窗口
    for window in all_windows:
        if window != main_window:
            driver.switch_to.window(window)
            break

    # 在新窗口中操作
    new_window_title = driver.title
    print(f"新窗口标题: {new_window_title}")

    # 执行所需操作...

    # 关闭新窗口
    driver.close()

    # 切换回主窗口
    driver.switch_to.window(main_window)

    # 验证回到主窗口
    print(f"当前窗口: {driver.title}")

场景四:数据抓取与爬虫

Selenium 不仅用于测试,还可以用于 Web 数据抓取:

"""
使用 Selenium 进行 Web 数据抓取
适合需要 JavaScript 渲染的动态页面
"""

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import TimeoutException
import json
import time
import re


class WebScraper:
    """Web 数据抓取器基类"""

    def __init__(self, headless=True):
        """初始化抓取器"""
        options = webdriver.ChromeOptions()
        if headless:
            options.add_argument("--headless=new")
        options.add_argument("--no-sandbox")
        options.add_argument("--disable-dev-shm-usage")

        # 反反爬虫措施
        options.add_argument("--disable-blink-features=AutomationControlled")
        options.add_experimental_option("excludeSwitches", ["enable-automation"])

        self.driver = webdriver.Chrome(options=options)
        self.wait = WebDriverWait(self.driver, 20)

    def scroll_page(self, scrolls=3, delay=2):
        """滚动页面加载更多内容"""
        for _ in range(scrolls):
            self.driver.execute_script(
                "window.scrollTo(0, document.body.scrollHeight);"
            )
            time.sleep(delay)

    def get_page_source(self):
        """获取页面源码"""
        return self.driver.page_source

    def save_to_json(self, data, filename):
        """保存数据到 JSON 文件"""
        with open(filename, 'w', encoding='utf-8') as f:
            json.dump(data, f, ensure_ascii=False, indent=2)
        print(f"数据已保存到 {filename}")

    def close(self):
        """关闭浏览器"""
        self.driver.quit()


class ProductScraper(WebScraper):
    """电商产品数据抓取"""

    def scrape_product_list(self, url, max_pages=5):
        """
        抓取产品列表页

        Args:
            url: 产品列表页 URL
            max_pages: 最大抓取页数

        Returns:
            产品数据列表
        """
        self.driver.get(url)
        time.sleep(3)  # 等待页面初始加载

        all_products = []

        for page in range(1, max_pages + 1):
            print(f"正在抓取第 {page} 页...")

            # 滚动加载
            self.scroll_page(scrolls=2, delay=2)

            # 等待产品列表加载
            try:
                products = self.wait.until(
                    EC.presence_of_all_elements_located(
                        By.CSS_SELECTOR, ".product-item"
                    )
                )
            except TimeoutException:
                print(f"第 {page} 页加载超时,跳过")
                continue

            # 提取产品信息
            for product in products:
                try:
                    product_data = {
                        "title": product.find_element(
                            By.CSS_SELECTOR, ".product-title"
                        ).text,
                        "price": product.find_element(
                            By.CSS_SELECTOR, ".product-price"
                        ).text,
                        "link": product.find_element(
                            By.CSS_SELECTOR, "a"
                        ).get_attribute("href"),
                        "image": product.find_element(
                            By.CSS_SELECTOR, "img"
                        ).get_attribute("src"),
                    }
                    all_products.append(product_data)
                except Exception as e:
                    print(f"提取产品信息失败: {e}")
                    continue

            # 点击下一页
            try:
                next_button = self.driver.find_element(
                    By.CSS_SELECTOR, ".pagination .next"
                )
                next_button.click()
                time.sleep(2)
            except:
                print("已到达最后一页")
                break

        return all_products

    def scrape_product_detail(self, url):
        """
        抓取单个产品详情

        Args:
            url: 产品详情页 URL

        Returns:
            产品详情数据
        """
        self.driver.get(url)
        time.sleep(2)

        detail = {
            "url": url,
            "title": self._safe_get_text(".product-title"),
            "price": self._safe_get_text(".price-current"),
            "original_price": self._safe_get_text(".price-original"),
            "description": self._safe_get_text(".product-description"),
            "specifications": self._get_specifications(),
            "images": self._get_product_images(),
        }

        return detail

    def _safe_get_text(self, selector):
        """安全获取元素文本"""
        try:
            element = self.driver.find_element(By.CSS_SELECTOR, selector)
            return element.text.strip()
        except:
            return None

    def _get_specifications(self):
        """获取产品规格"""
        specs = {}
        try:
            spec_rows = self.driver.find_elements(
                By.CSS_SELECTOR, ".spec-table tr"
            )
            for row in spec_rows:
                cells = row.find_elements(By.TAG_NAME, "td")
                if len(cells) >= 2:
                    key = cells[0].text.strip()
                    value = cells[1].text.strip()
                    specs[key] = value
        except:
            pass
        return specs

    def _get_product_images(self):
        """获取产品图片列表"""
        images = []
        try:
            img_elements = self.driver.find_elements(
                By.CSS_SELECTOR, ".product-images img"
            )
            for img in img_elements:
                src = img.get_attribute("src")
                if src:
                    images.append(src)
        except:
            pass
        return images


# ==== 社交媒体数据抓取示例 ====
class SocialMediaScraper(WebScraper):
    """社交媒体数据抓取"""

    def scrape_posts(self, hashtag_url):
        """
        抓取某个话题下的帖子

        Args:
            hashtag_url: 话题页面 URL

        Returns:
            帖子数据列表
        """
        self.driver.get(hashtag_url)

        # 滚动加载
        last_height = self.driver.execute_script(
            "return document.body.scrollHeight"
        )

        posts = []
        scroll_count = 0
        max_scrolls = 20

        while scroll_count < max_scrolls:
            # 滚动到页面底部
            self.driver.execute_script(
                "window.scrollTo(0, document.body.scrollHeight);"
            )
            time.sleep(2)

            # 获取当前可见帖子
            post_elements = self.driver.find_elements(
                By.CSS_SELECTOR, ".post-item"
            )

            for post_el in post_elements:
                try:
                    post_data = {
                        "author": post_el.find_element(
                            By.CSS_SELECTOR, ".post-author"
                        ).text,
                        "content": post_el.find_element(
                            By.CSS_SELECTOR, ".post-content"
                        ).text,
                        "likes": post_el.find_element(
                            By.CSS_SELECTOR, ".post-likes"
                        ).text,
                        "timestamp": post_el.find_element(
                            By.CSS_SELECTOR, ".post-time"
                        ).get_attribute("datetime"),
                    }
                    posts.append(post_data)
                except:
                    continue

            # 检查是否还有新内容
            new_height = self.driver.execute_script(
                "return document.body.scrollHeight"
            )
            if new_height == last_height:
                break

            last_height = new_height
            scroll_count += 1
            print(f"滚动 {scroll_count} 次,获取 {len(posts)} 条帖子")

        return posts


# ==== 使用示例 ====
if __name__ == "__main__":
    scraper = ProductScraper(headless=True)

    try:
        # 抓取产品列表
        products = scraper.scrape_product_list(
            "https://example.com/products",
            max_pages=3
        )

        # 保存结果
        scraper.save_to_json(products, "products.json")

        print(f"共抓取 {len(products)} 个产品")

    finally:
        scraper.close()

技巧与最佳实践

性能优化技巧

1. 合理使用显式等待而非硬等待

# ❌ 不推荐:硬等待(固定时间)
import time
time.sleep(5)  # 不管元素是否加载完成都等待 5 秒

# ✓ 推荐:显式等待
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)  # 最多等待 10 秒
element = wait.until(
    EC.presence_of_element_located((By.ID, "element-id"))
)

2. 使用隐式等待配合显式等待

# 设置隐式等待(全局生效,针对元素查找)
driver.implicitly_wait(10)

# 设置显式等待(针对特定条件的等待)
wait = WebDriverWait(driver, 20)
element = wait.until(EC.element_to_be_clickable((By.ID, "btn")))

3. 批量操作优化

# ❌ 低效:逐个查找和操作
for item in items:
    element = driver.find_element(By.ID, item)
    element.click()

# ✓ 高效:一次性查找所有元素
elements = driver.find_elements(By.CSS_SELECTOR, ".items")
for element in elements:
    # 对每个元素执行操作
    pass

4. JavaScript 执行优化

# 对于复杂操作,使用 JavaScript 可能比 Selenium API 更快
# 但要注意这会跳过 Selenium 的等待机制

# 例:滚动到页面底部
driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")

# 例:直接设置输入框值(绕过键盘事件)
driver.execute_script(
    "arguments[0].value = arguments[1];",
    element,
    "new value"
)

稳定性提升技巧

1. 元素定位策略选择

# 定位策略优先级(从高到低):
# 1. ID - 最稳定,唯一性最强
element = driver.find_element(By.ID, "unique-id")

# 2. NAME - 较稳定
element = driver.find_element(By.NAME, "form-field")

# 3. CSS_SELECTOR - 性能和可读性兼顾
element = driver.find_element(By.CSS_SELECTOR, "div.container > ul.items li")

# 4. XPATH - 最灵活,但可能较慢
element = driver.find_element(By.XPATH, "//div[contains(@class, 'container')]//a")

# 5. LINK_TEXT - 用于链接
element = driver.find_element(By.LINK_TEXT, "点击这里")

# 避免使用:CLASS_NAME(可能有多个匹配)、TAG_NAME(过于宽泛)

2. 构建健壮的 XPATH

# ✓ 推荐:使用相对路径和多个属性
xpath = "//div[@class='product-card' and @data-active='true']//span[@class='price']"

# ✓ 推荐:使用 contains() 函数处理动态内容
xpath = "//div[contains(@class, 'product-')]//span[contains(text(), '价格')]"

# ✓ 推荐:使用 starts-with() 处理前缀
xpath = "//div[starts-with(@id, 'item-')]"

# ✓ 推荐:使用 position() 获取特定位置
xpath = "//ul[@class='menu']/li[position() = 1]"  # 第一个
xpath = "//ul[@class='menu']/li[last()]"  # 最后一个

# ✓ 推荐:使用 ancestor 获取祖先元素
xpath = "//span[@class='price']/ancestor::div[@class='product-card']"

3. 添加重试机制

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import StaleElementReferenceException
import time

def find_element_with_retry(driver, by, value, retries=3, delay=1):
    """
    带重试的元素查找

    Args:
        driver: WebDriver 实例
        by: 定位方式
        value: 定位值
        retries: 重试次数
        delay: 重试间隔(秒)

    Returns:
        WebElement
    """
    for attempt in range(retries):
        try:
            element = driver.find_element(by, value)
            return element
        except StaleElementReferenceException:
            # 元素过期,需要重新查找
            if attempt < retries - 1:
                time.sleep(delay)
                continue
            else:
                raise

    raise Exception(f"无法找到元素: {by}={value}")

4. 处理 StaleElementReferenceException

# 问题原因:DOM 更新导致元素引用失效
# 解决方案:重新查找或使用列表方式

# 方法一:捕获异常并重试
def safe_click(driver, locator):
    """安全的点击操作"""
    max_attempts = 3
    for i in range(max_attempts):
        try:
            element = driver.find_element(*locator)
            element.click()
            return True
        except StaleElementReferenceException:
            if i == max_attempts - 1:
                raise
            time.sleep(0.5)
    return False

# 方法二:使用列表一次性获取所有引用
# 在操作前先获取所有元素
elements = driver.find_elements(By.CSS_SELECTOR, ".menu-item")
for el in elements:
    try:
        text = el.text
        print(text)
    except StaleElementReferenceException:
        continue  # 跳过失效元素

反反爬虫与高级技巧

1. 绕过自动化检测

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.chrome.service import Service

def create_stealth_driver():
    """创建反检测的 WebDriver"""
    options = Options()

    # 基础配置
    options.add_argument("--headless=new")
    options.add_argument("--no-sandbox")
    options.add_argument("--disable-dev-shm-usage")
    options.add_argument("--disable-gpu")
    options.add_argument("--window-size=1920,1080")

    # 修改 WebDriver 标识
    options.add_argument("--disable-blink-features=AutomationControlled")
    options.add_experimental_option("excludeSwitches", ["enable-automation"])
    options.add_experimental_option("useAutomationExtension", False)

    # 设置 User-Agent
    options.add_argument(
        "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
        "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
    )

    # 创建驱动
    driver = webdriver.Chrome(options=options)

    # 执行 CDP 命令隐藏自动化特征
    driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
        "source": """
            Object.defineProperty(navigator, 'webdriver', {
                get: () => undefined
            });
            Object.defineProperty(navigator, 'plugins', {
                get: () => [1, 2, 3, 4, 5]
            });
            Object.defineProperty(navigator, 'languages', {
                get: () => ['zh-CN', 'zh', 'en']
            });
            window.chrome = {
                runtime: {}
            };
        """
    })

    return driver

2. 使用代理服务器

def create_driver_with_proxy(proxy_server):
    """使用代理创建 WebDriver"""
    options = Options()
    options.add_argument(f"--proxy-server={proxy_server}")
    # proxy_server 格式: "http://username:password@proxy.com:8080"

    driver = webdriver.Chrome(options=options)
    return driver

3. 处理验证码

# 方法一:使用第三方验证码识别服务
def solve_captcha_with_service(image_path, service="2captcha"):
    """
    使用验证码识别服务

    需要注册第三方服务(如 2Captcha、Anti-Captcha)获取 API Key
    """
    # 这里以伪代码示意
    if service == "2captcha":
        # 调用 2Captcha API
        captcha_text = call_2captcha_api(image_path)
    elif service == "anti-captcha":
        # 调用 Anti-Captcha API
        captcha_text = call_anticaptcha_api(image_path)

    return captcha_text

# 方法二:手动模式(需要人工介入)
def solve_captcha_manually(driver, captcha_element):
    """
    人工解决验证码

    暂停自动化,等待用户手动输入
    """
    captcha_element.screenshot("captcha.png")
    print("请查看 screenshots/captcha.png 并输入验证码:")
    captcha_solution = input()
    return captcha_solution

测试框架集成

1. 与 pytest 深度集成

# conftest.py - pytest 配置和共享 fixtures
import pytest
from selenium import webdriver
from selenium.webdriver.chrome.service import Service

@pytest.fixture(scope="session")
def browser():
    """会话级别的浏览器 fixture"""
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")

    driver = webdriver.Chrome(service=Service(), options=options)
    yield driver
    driver.quit()

@pytest.fixture
def authenticated_browser(browser):
    """
    已登录认证的浏览器 fixture

    使用 scope="function" 确保每个测试都是独立状态
    """
    # 执行登录操作
    browser.get("https://example.com/login")
    browser.find_element(By.NAME, "username").send_keys("testuser")
    browser.find_element(By.NAME, "password").send_keys("password")
    browser.find_element(By.TYPE, "submit").click()

    # 等待登录成功
    from selenium.webdriver.support.ui import WebDriverWait
    from selenium.webdriver.support import expected_conditions as EC

    wait = WebDriverWait(browser, 10)
    wait.until(EC.url_to_be("https://example.com/dashboard"))

    yield browser

    # 测试后清理:登出
    browser.get("https://example.com/logout")


@pytest.fixture
def test_data():
    """测试数据 fixture"""
    return {
        "user": {
            "username": "test_user",
            "email": "test@example.com",
            "password": "SecureP@ss123"
        },
        "product": {
            "name": "测试产品",
            "price": 99.99,
            "quantity": 2
        }
    }

2. 参数化测试

import pytest
from selenium.webdriver.common.by import By

# pytest 参数化装饰器
@pytest.mark.parametrize("search_keyword,expected_count", [
    ("Selenium", 100),
    ("Python", 200),
    ("Automation Testing", 50),
    ("WebDriver", 30),
])
def test_search_results(browser, search_keyword, expected_count):
    """
    参数化搜索测试

    使用不同的关键词进行测试
    """
    browser.get("https://www.baidu.com")

    # 输入搜索词
    search_input = browser.find_element(By.ID, "kw")
    search_input.send_keys(search_keyword)
    search_input.submit()

    # 验证结果
    # ...断言逻辑


# 多参数组合测试
@pytest.mark.parametrize("browser_name,viewport_size", [
    ("chrome", (1920, 1080)),
    ("chrome", (1366, 768)),
    ("firefox", (1920, 1080)),
    ("edge", (1280, 720)),
])
def test_responsive_design(browser_name, viewport_size):
    """
    响应式设计测试

    测试不同浏览器和视口大小
    """
    width, height = viewport_size
    # 设置视口大小
    browser.set_window_size(width, height)

    # 验证响应式布局
    # ...

3. 测试报告生成

# 使用 pytest-html 生成 HTML 报告
# 安装: pip install pytest-html

# 运行测试并生成报告:
# pytest tests/ --html=report.html --self-contained-html

# 使用 pytest-screenshot 生成截图报告
# 安装: pip install pytest-screenshot

# conftest.py
import pytest
from pytest_screenshot import screenshot

@pytest.hookimpl(tryfirst=True, 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("browser")
        if driver:
            screenshot.take_screenshot(driver, report)

调试技巧

1. 添加详细日志

import logging
import sys

# 配置日志
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('selenium_debug.log', encoding='utf-8'),
        logging.StreamHandler(sys.stdout)
    ]
)

logger = logging.getLogger(__name__)

def log_step(step_name):
    """记录测试步骤"""
    logger.info(f"=== 执行步骤: {step_name} ===")

def log_element_info(element):
    """记录元素信息"""
    logger.debug(f"元素信息: tag={element.tag_name}, "
                 f"text={element.text[:50]}, "
                 f"visible={element.is_displayed()}")

2. 失败时自动截图和保存源码

import os
from datetime import datetime

def save_debug_info(driver, test_name):
    """
    保存调试信息

    在测试失败时调用,保存截图和页面源码
    """
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    debug_dir = f"debug/{test_name}_{timestamp}"
    os.makedirs(debug_dir, exist_ok=True)

    # 保存截图
    screenshot_path = os.path.join(debug_dir, "screenshot.png")
    driver.save_screenshot(screenshot_path)

    # 保存页面源码
    source_path = os.path.join(debug_dir, "page_source.html")
    with open(source_path, 'w', encoding='utf-8') as f:
        f.write(driver.page_source)

    print(f"调试信息已保存到: {debug_dir}")


# 使用 try-finally 确保清理
try:
    # 测试代码
    driver.get("https://example.com")
    # ...
except Exception as e:
    save_debug_info(driver, "test_name")
    raise

3. 使用 Selenium IDE 录制脚本

Selenium IDE 是 Chrome 和 Firefox 的扩展程序,可以录制用户操作并生成脚本:

  1. 安装 Selenium IDE
  2. 点击扩展图标开始录制
  3. 执行要测试的操作
  4. 停止录制
  5. 导出为 Python 代码

导出的代码可作为参考,帮助你理解元素的定位方式。


总结与资源链接

核心要点回顾

通过本教程,我们学习了 Selenium 的完整使用方法:

环境搭建

  • 安装 Python 和 Selenium 包
  • 下载配置浏览器 WebDriver
  • 验证安装成功

核心概念

  • WebDriver 架构与原理
  • 元素定位策略(8 种方式)
  • 显式等待与隐式等待
  • Page Object 设计模式

实战技能

  • 表单填写与提交
  • 弹窗与对话框处理
  • iframe 和多窗口切换
  • 页面滚动与动态内容
  • 数据抓取与爬虫

进阶技巧

  • 性能优化
  • 稳定性提升
  • 反反爬虫策略
  • 测试框架集成
  • 调试与日志

官方文档与学习资源

Selenium 官方资源

  • 官方文档:https://www.selenium.dev/documentation/
  • GitHub 仓库:https://github.com/SeleniumHQ/selenium
  • Selenium IDE:https://www.selenium.dev/selenium-ide/
  • Selenium Grid:https://www.selenium.dev/documentation/grid/

社区与支持

  • Stack Overflow(Selenium 标签):https://stackoverflow.com/questions/tagged/selenium
  • Selenium Discord 社区:https://discord.gg/Selenium
  • Reddit r/selenium:https://www.reddit.com/r/selenium/

推荐学习路径

  1. 官方文档入门教程
  2. 本教程的实战项目练习
  3. 参与开源测试项目贡献
  4. 学习测试框架集成(pytest)

相关项目推荐

测试框架

  • pytest:Python 最流行的测试框架,与 Selenium 深度集成
  • GitHub: https://github.com/pytest-dev/pytest

  • Robot Framework:关键词驱动的测试框架

  • GitHub: https://github.com/robotframework/RobotFramework

浏览器自动化替代方案

  • Playwright:微软出品的现代自动化工具,支持更多现代特性
  • GitHub: https://github.com/microsoft/playwright

  • Puppeteer:Chrome 官方出品的 Node.js 自动化库

  • GitHub: https://github.com/puppeteer/puppeteer

  • Cypress:专为 Web 应用测试设计的框架

  • GitHub: https://github.com/cypress-io/cypress

辅助工具

  • webdriver-manager:自动管理 WebDriver 版本的 Python 库
  • GitHub: https://github.com/SergeyPirogov/webdriver_manager

  • selenium-requests:结合 Requests 库使用 Selenium

  • GitHub: https://github.com/cryzed/Selenium-requests

下一步学习建议

  1. 深入理解 Page Object 模式:学习如何构建更复杂的页面对象层级结构

  2. 掌握测试数据管理:学习如何使用 JSON、YAML、CSV 等格式管理测试数据

  3. CI/CD 集成:将 Selenium 测试集成到 Jenkins、GitHub Actions 等 CI/CD 流水线

  4. 分布式测试:学习使用 Selenium Grid 进行跨浏览器、跨机器的分布式测试

  5. 移动端测试:学习使用 Appium 进行移动端 Web 和原生应用的自动化测试

Selenium 是 Web 自动化领域的基石,掌握它不仅能提升你的测试效率,还能帮助你深入理解 Web 应用的运行原理。希望本教程能成为你学习道路上的有力助手。

祝你学习愉快,测试顺利!


附录 A:常见错误代码参考

错误代码 含义 解决方案
NoSuchElementException 找不到元素 检查定位器是否正确,增加等待时间
StaleElementReferenceException 元素已过期 重新查找元素或添加重试机制
TimeoutException 操作超时 增加等待时间或检查网络
ElementNotInteractableException 元素不可交互 确保元素可见,可能需要滚动到可见区域
InvalidSelectorException 选择器无效 检查 XPath 或 CSS 选择器语法
WebDriverException WebDriver 异常 检查浏览器版本与驱动版本是否匹配

附录 B:快捷键速查

快捷键 功能
Keys.ENTER 回车键
Keys.TAB Tab 键
Keys.CONTROL + 'a' 全选
Keys.CONTROL + 'c' 复制
Keys.CONTROL + 'v' 粘贴
Keys.CONTROL + 'z' 撤销
Keys.ESCAPE 按 Escape 键
Keys.ARROW_UP/DOWN/LEFT/RIGHT 方向键

如果内容对您有帮助,欢迎打赏

您的支持是我继续创作的动力

前往打赏页面

评论区

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注