软件测试专栏(6/20):Selenium从0到1实战指南:元素定位、等待机制与框架封装

本文导读:Selenium是Web自动化测试的事实标准,但初学者常常被元素定位失败、脚本不稳定、维护成本高等问题困扰。本文将从实战角度出发,带你深入理解Selenium的核心技术,掌握八大定位策略、三种等待机制,并最终构建一个企业级的自动化测试框架。


一、Selenium生态全景:不仅仅是WebDriver

1.1 Selenium家族成员

Selenium生态
Selenium WebDriver
Selenium IDE
Selenium Grid
"核心组件

浏览器自动化API"
"支持语言

Java/Python/JS/C#/Ruby"
"浏览器插件

录制回放工具"
"适合场景

快速原型、教学演示"
"分布式测试

并行执行"
"跨浏览器/平台

远程执行"

1.2 Selenium的工作原理

python 复制代码
"""
Selenium WebDriver工作原理:
1. 测试脚本通过WebDriver API发送命令
2. 浏览器驱动(ChromeDriver/GeckoDriver)接收命令
3. 驱动将命令转换为浏览器的原生指令
4. 浏览器执行操作并返回结果
5. 结果通过驱动传回测试脚本
"""

二、元素定位:自动化测试的基石

2.1 八大定位策略详解

Selenium提供了8种传统定位策略,每种都有其适用场景:

定位策略 方法 适用场景 优缺点
ID By.ID 元素有唯一ID ⭐⭐⭐⭐⭐ 最快、最稳定
Name By.NAME 表单元素有name属性 ⭐⭐⭐⭐ 次之,但可能不唯一
Class Name By.CLASS_NAME 元素有特定class ⭐⭐ 复合类名不支持
Tag Name By.TAG_NAME 批量获取同类型元素 ⭐ 不精确,易匹配多个
Link Text By.LINK_TEXT 精确匹配链接文本 ⭐⭐⭐ 适合a标签
Partial Link Text By.PARTIAL_LINK_TEXT 模糊匹配链接文本 ⭐⭐ 可能匹配多个
CSS Selector By.CSS_SELECTOR 复杂定位需求 ⭐⭐⭐⭐⭐ 灵活强大
XPath By.XPATH 无法用其他方式定位 ⭐⭐⭐ 强大但性能稍差

2.2 实战示例:百度搜索页定位

python 复制代码
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
import time

class BaiduPageLocators:
    """百度首页元素定位器"""
    
    # 1. ID定位 - 最推荐的方式
    SEARCH_INPUT_ID = (By.ID, "kw")
    
    # 2. Name定位 - 表单元素常用
    SEARCH_INPUT_NAME = (By.NAME, "wd")
    
    # 3. Class Name定位 - 注意:不支持复合类名
    SEARCH_BUTTON_CLASS = (By.CLASS_NAME, "s_btn")
    
    # 4. CSS Selector定位 - 灵活强大
    SEARCH_INPUT_CSS = (By.CSS_SELECTOR, "#kw")
    SEARCH_BUTTON_CSS = (By.CSS_SELECTOR, "input[type='submit']")
    
    # 5. XPath定位 - 绝对路径和相对路径
    SEARCH_INPUT_XPATH_ABSOLUTE = (By.XPATH, "/html/body/div[1]/div[3]/form/span[1]/input")
    SEARCH_INPUT_XPATH_RELATIVE = (By.XPATH, "//input[@id='kw']")
    
    # 6. 链接文本定位 - 精确匹配
    NEWS_LINK = (By.LINK_TEXT, "新闻")
    
    # 7. 部分链接文本定位 - 模糊匹配
    MAP_LINK_PARTIAL = (By.PARTIAL_LINK_TEXT, "地图")
    
    # 8. 标签名定位 - 获取所有input元素
    ALL_INPUTS = (By.TAG_NAME, "input")

class BaiduSearchTest:
    """百度搜索测试类"""
    
    def __init__(self):
        self.driver = webdriver.Chrome()
        self.driver.maximize_window()
        self.wait = WebDriverWait(self.driver, 10)
    
    def test_all_locators(self):
        """演示各种定位方法的使用"""
        
        # 打开百度首页
        self.driver.get("https://www.baidu.com")
        time.sleep(2)  # 等待页面加载
        
        # 1. ID定位 - 输入搜索关键词
        search_input = self.driver.find_element(*BaiduPageLocators.SEARCH_INPUT_ID)
        search_input.clear()
        search_input.send_keys("Selenium 定位实战")
        print("✓ ID定位成功")
        
        # 2. Class Name定位 - 点击搜索按钮
        search_btn = self.driver.find_element(*BaiduPageLocators.SEARCH_BUTTON_CLASS)
        search_btn.click()
        print("✓ Class Name定位成功")
        
        time.sleep(3)
        self.driver.back()  # 返回首页
        
        # 3. CSS Selector定位 - 更灵活的定位方式
        search_input_css = self.driver.find_element(*BaiduPageLocators.SEARCH_INPUT_CSS)
        search_input_css.clear()
        search_input_css.send_keys("CSS选择器测试")
        print("✓ CSS Selector定位成功")
        
        search_btn_css = self.driver.find_element(*BaiduPageLocators.SEARCH_BUTTON_CSS)
        search_btn_css.click()
        print("✓ CSS选择器按钮定位成功")
        
        time.sleep(3)
        self.driver.back()
        
        # 4. XPath定位 - 处理复杂结构
        search_input_xpath = self.driver.find_element(*BaiduPageLocators.SEARCH_INPUT_XPATH_RELATIVE)
        search_input_xpath.clear()
        search_input_xpath.send_keys("XPath测试")
        print("✓ XPath定位成功")
        
        # 5. 链接文本定位 - 点击新闻链接
        news_link = self.driver.find_element(*BaiduPageLocators.NEWS_LINK)
        print(f"✓ 链接文本定位成功: {news_link.text}")
        
        # 6. 获取所有input元素
        all_inputs = self.driver.find_elements(*BaiduPageLocators.ALL_INPUTS)
        print(f"✓ 页面上共有 {len(all_inputs)} 个input元素")
    
    def tearDown(self):
        """测试清理"""
        time.sleep(3)
        self.driver.quit()

# 运行测试
if __name__ == "__main__":
    test = BaiduSearchTest()
    test.test_all_locators()
    test.tearDown()

2.3 定位策略选择指南

python 复制代码
class LocatorStrategyGuide:
    """定位策略选择指南"""
    
    @staticmethod
    def recommend_strategy(element_info):
        """
        根据元素特征推荐最佳定位策略
        
        Args:
            element_info: 元素特征字典
        """
        
        recommendations = []
        
        # 1. 如果有唯一ID,首选ID
        if element_info.get('has_unique_id'):
            recommendations.append({
                'strategy': 'ID',
                'priority': '最高',
                'reason': 'ID在页面中唯一,定位最快最稳定',
                'example': 'driver.find_element(By.ID, "username")'
            })
        
        # 2. 如果有唯一Name属性,次选Name
        elif element_info.get('has_unique_name'):
            recommendations.append({
                'strategy': 'Name',
                'priority': '高',
                'reason': 'Name属性通常是唯一的,特别是表单元素',
                'example': 'driver.find_element(By.NAME, "email")'
            })
        
        # 3. 如果是链接元素
        elif element_info.get('is_link'):
            if element_info.get('exact_text_match'):
                recommendations.append({
                    'strategy': 'Link Text',
                    'priority': '中',
                    'reason': '精确匹配链接文本,直观易懂',
                    'example': 'driver.find_element(By.LINK_TEXT, "登录")'
                })
            else:
                recommendations.append({
                    'strategy': 'Partial Link Text',
                    'priority': '中',
                    'reason': '适用于动态生成的部分链接文本',
                    'example': 'driver.find_element(By.PARTIAL_LINK_TEXT, "注册")'
                })
        
        # 4. 默认推荐CSS Selector
        recommendations.append({
            'strategy': 'CSS Selector',
            'priority': '推荐通用方案',
            'reason': 'CSS选择器灵活强大,性能好,可读性强',
            'example': 'driver.find_element(By.CSS_SELECTOR, ".btn-primary[type=\'submit\']")'
        })
        
        # 5. XPath作为备选方案
        recommendations.append({
            'strategy': 'XPath',
            'priority': '备选方案',
            'reason': 'XPath功能最强大,但性能稍差,适合复杂DOM结构',
            'example': 'driver.find_element(By.XPATH, "//div[@class=\'container\']//button[text()=\'提交\']")'
        })
        
        return recommendations

# 示例:为不同元素推荐定位策略
element_cases = [
    {"name": "登录按钮", "has_unique_id": True, "is_link": False},
    {"name": "邮箱输入框", "has_unique_name": True, "is_link": False},
    {"name": "关于我们链接", "is_link": True, "exact_text_match": True},
    {"name": "动态生成的菜单项", "is_link": True, "exact_text_match": False},
    {"name": "复杂表格中的单元格", "has_unique_id": False, "is_link": False}
]

for case in element_cases:
    print(f"\n=== 为 '{case['name']}' 推荐的定位策略 ===")
    recs = LocatorStrategyGuide.recommend_strategy(case)
    for r in recs[:2]:  # 只显示前两个推荐
        print(f"【{r['strategy']}】{r['priority']}: {r['reason']}")
        print(f"  示例: {r['example']}")

2.4 高级定位技巧

python 复制代码
class AdvancedLocatorTechniques:
    """高级定位技巧"""
    
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
    
    def locate_by_multiple_attributes(self):
        """组合多个属性定位"""
        
        # CSS选择器组合属性
        element = self.driver.find_element(By.CSS_SELECTOR, 
            "input[type='text'][name='username'][data-role='user-input']")
        
        # XPath组合条件
        element = self.driver.find_element(By.XPATH, 
            "//input[@type='text' and @name='username' and contains(@class, 'input-field')]")
        
        return element
    
    def locate_by_text_content(self):
        """通过文本内容定位"""
        
        # 精确文本匹配
        element = self.driver.find_element(By.XPATH, 
            "//button[text()='提交订单']")
        
        # 包含文本匹配
        element = self.driver.find_element(By.XPATH, 
            "//div[contains(text(), '验证码已发送')]")
        
        # 忽略大小写匹配
        element = self.driver.find_element(By.XPATH, 
            "//*[translate(text(), 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz') = 'success']")
        
        return element
    
    def locate_by_hierarchy(self):
        """通过层级关系定位"""
        
        # 父节点 -> 子节点
        element = self.driver.find_element(By.CSS_SELECTOR, 
            "div.container > ul > li.active > a")
        
        # 兄弟节点
        element = self.driver.find_element(By.XPATH, 
            "//label[@for='username']/following-sibling::input")
        
        # 祖先节点
        element = self.driver.find_element(By.XPATH, 
            "//input[@id='submit']/ancestor::form")
        
        return element
    
    def locate_by_position(self):
        """通过位置索引定位"""
        
        # 第一个元素
        first = self.driver.find_element(By.CSS_SELECTOR, 
            "ul.products li:first-child")
        
        # 最后一个元素
        last = self.driver.find_element(By.CSS_SELECTOR, 
            "ul.products li:last-child")
        
        # 第n个元素(nth-child)
        third = self.driver.find_element(By.CSS_SELECTOR, 
            "ul.products li:nth-child(3)")
        
        # 奇数/偶数元素
        odd_rows = self.driver.find_elements(By.CSS_SELECTOR, 
            "table tr:nth-child(odd)")
        even_rows = self.driver.find_elements(By.CSS_SELECTOR, 
            "table tr:nth-child(even)")
        
        return first, last, third
    
    def locate_dynamic_elements(self):
        """定位动态元素"""
        
        # 包含动态ID的部分匹配
        element = self.driver.find_element(By.CSS_SELECTOR, 
            "div[id^='temp_']")  # ID以temp_开头
        
        element = self.driver.find_element(By.CSS_SELECTOR, 
            "div[id$='_container']")  # ID以_container结尾
        
        element = self.driver.find_element(By.CSS_SELECTOR, 
            "div[id*='_user_']")  # ID包含_user_
        
        return element

三、等待机制:解决不稳定测试的关键

3.1 为什么需要等待?

大多数Web应用程序使用Ajax和JavaScript动态加载内容。当浏览器加载页面时,元素可能在不同时间间隔加载,这导致了竞态条件 ------脚本执行速度和页面加载速度的竞赛。不恰当的等待是导致不稳定测试的主要来源。

3.2 三种等待策略对比

等待类型 实现方式 作用范围 优缺点 适用场景
强制等待 time.sleep() 当前线程 ❌ 固定时间,效率低 几乎不推荐使用
隐式等待 driver.implicitly_wait() 全局,所有元素 ⭐⭐ 简单但粗粒度 作为基础等待
显式等待 WebDriverWait + ExpectedConditions 特定元素 ⭐⭐⭐⭐⭐ 精准控制 核心交互元素

3.3 强制等待:最原始的方式

python 复制代码
import time

class ForceWaitDemo:
    """强制等待演示 - 不推荐使用"""
    
    def bad_practice(self):
        """糟糕的实践"""
        driver = webdriver.Chrome()
        driver.get("https://example.com")
        
        #  固定等待,不管页面是否加载完成
        time.sleep(5)  # 如果页面2秒加载完,浪费3秒
        # 如果页面6秒加载完,5秒不够,测试失败
        
        element = driver.find_element(By.ID, "dynamic-content")
        element.click()
        
        #  到处都是time.sleep
        time.sleep(3)
        another_element = driver.find_element(By.CLASS_NAME, "result")
        
        driver.quit()
    
    def better_approach(self):
        """更好的实践"""
        driver = webdriver.Chrome()
        driver.get("https://example.com")
        
        #  使用显式等待替代固定等待
        from selenium.webdriver.support.ui import WebDriverWait
        from selenium.webdriver.support import expected_conditions as EC
        
        wait = WebDriverWait(driver, 10)
        element = wait.until(
            EC.element_to_be_clickable((By.ID, "dynamic-content"))
        )
        element.click()
        
        # 等待下一个元素
        result = wait.until(
            EC.visibility_of_element_located((By.CLASS_NAME, "result"))
        )
        
        driver.quit()

3.4 隐式等待:全局设置

隐式等待告诉WebDriver在抛出异常之前等待一定时间。这是一个全局设置,适用于整个会话期间的每个元素定位调用。

python 复制代码
class ImplicitWaitDemo:
    """隐式等待演示"""
    
    def setup_implicit_wait(self):
        """设置隐式等待"""
        driver = webdriver.Chrome()
        
        # 设置隐式等待为10秒
        driver.implicitly_wait(10)
        
        # 一旦设置,在整个会话期间都有效
        driver.get("https://example.com")
        
        # 这个find_element会等待最多10秒,直到元素出现
        element1 = driver.find_element(By.ID, "element1")
        
        # 这个也会等待最多10秒
        element2 = driver.find_element(By.CLASS_NAME, "element2")
        
        return driver
    
    def implicit_wait_mechanism(self):
        """
        隐式等待的工作原理:
        1. 立即检查元素是否存在
        2. 如果存在,立即返回
        3. 如果不存在,每隔500ms轮询一次
        4. 在超时时间内,一旦元素出现就立即返回
        5. 如果超时仍未出现,抛出NoSuchElementException
        """
        driver = webdriver.Chrome()
        driver.implicitly_wait(10)
        
        import time
        start = time.time()
        
        try:
            # 尝试定位一个不存在的元素
            element = driver.find_element(By.ID, "non_existent_element")
        except:
            elapsed = time.time() - start
            print(f"等待了 {elapsed:.2f} 秒后抛出异常")
            # 输出:等待了 10.01 秒后抛出异常
        
        driver.quit()
    
    def implicit_wait_caveats(self):
        """隐式等待的注意事项"""
        
        #  警告1:不要混合使用隐式等待和显式等待
        driver = webdriver.Chrome()
        driver.implicitly_wait(10)  # 设置隐式等待10秒
        
        wait = WebDriverWait(driver, 15)  # 显式等待15秒
        
        # 这可能会导致意想不到的等待时间(可能长达20-25秒)
        element = wait.until(
            EC.presence_of_element_located((By.ID, "some-element"))
        )
        
        #  警告2:隐式等待对查找元素有效,但对元素状态(如可点击)无效
        # 需要等待元素可点击时,仍需显式等待
        
        driver.quit()

3.5 显式等待:精准控制

显式等待是在代码中添加的循环,用于轮询应用程序,直到特定条件评估为真。这是处理复杂等待场景的最佳选择。

python 复制代码
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException, ElementNotInteractableException

class ExplicitWaitDemo:
    """显式等待演示"""
    
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
    
    def wait_for_presence(self):
        """等待元素存在(在DOM中存在)"""
        element = self.wait.until(
            EC.presence_of_element_located((By.ID, "dynamic-element"))
        )
        return element
    
    def wait_for_visibility(self):
        """等待元素可见(存在且显示)"""
        element = self.wait.until(
            EC.visibility_of_element_located((By.CLASS_NAME, "loading-content"))
        )
        return element
    
    def wait_for_clickable(self):
        """等待元素可点击(可见且启用)"""
        button = self.wait.until(
            EC.element_to_be_clickable((By.ID, "submit-button"))
        )
        return button
    
    def wait_for_text_present(self):
        """等待元素包含特定文本"""
        element = self.wait.until(
            EC.text_to_be_present_in_element((By.ID, "status"), "成功")
        )
        return element
    
    def wait_for_element_to_disappear(self):
        """等待元素消失(如加载动画)"""
        success = self.wait.until(
            EC.invisibility_of_element_located((By.CLASS_NAME, "loading-spinner"))
        )
        return success
    
    def wait_for_alert(self):
        """等待弹窗出现"""
        alert = self.wait.until(EC.alert_is_present())
        return alert
    
    def wait_for_frame_and_switch(self):
        """等待iframe可用并切换"""
        frame = self.wait.until(
            EC.frame_to_be_available_and_switch_to_it((By.ID, "frame-id"))
        )
        return frame
    
    def wait_for_title_contains(self):
        """等待标题包含特定文本"""
        success = self.wait.until(EC.title_contains("Dashboard"))
        return success
    
    def wait_for_url_contains(self):
        """等待URL包含特定文本"""
        success = self.wait.until(EC.url_contains("success.html"))
        return success
    
    def wait_for_multiple_conditions(self):
        """等待多个条件组合"""
        
        # 等待元素出现且可点击
        element = self.wait.until(
            lambda driver: (
                EC.presence_of_element_located((By.ID, "my-element"))(driver) and
                EC.element_to_be_clickable((By.ID, "my-element"))(driver)
            )
        )
        return element
    
    def custom_wait_condition(self):
        """自定义等待条件"""
        
        def element_has_css_class(driver):
            """自定义条件:检查元素是否有特定CSS类"""
            element = driver.find_element(By.ID, "my-element")
            return "active" in element.get_attribute("class")
        
        # 使用自定义条件
        result = self.wait.until(element_has_css_class)
        return result
    
    def wait_with_custom_message(self):
        """带自定义错误信息的等待"""
        try:
            element = self.wait.until(
                EC.presence_of_element_located((By.ID, "critical-element"))
            )
            return element
        except TimeoutException:
            # 自定义超时信息
            self.driver.save_screenshot("timeout_error.png")
            print("等待超时:关键元素未在10秒内加载")
            raise

3.6 Fluent Wait:高度可定制的等待

Fluent Wait是显式等待的增强版,允许设置轮询频率、忽略特定异常等。

python 复制代码
from selenium.webdriver.support.ui import WebDriverWait
from selenium.common.exceptions import NoSuchElementException, ElementNotInteractableException
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

class FluentWaitDemo:
    """Fluent Wait等待演示"""
    
    def setup_fluent_wait(self, driver):
        """配置Fluent Wait"""
        
        # 创建Fluent Wait实例
        wait = WebDriverWait(
            driver,
            timeout=10,           # 总超时时间10秒
            poll_frequency=0.5,    # 每0.5秒轮询一次
            ignored_exceptions=[
                NoSuchElementException,           # 忽略元素不存在异常
                ElementNotInteractableException    # 忽略元素不可交互异常
            ]
        )
        
        return wait
    
    def wait_with_fluent(self, driver):
        """使用Fluent Wait等待"""
        
        wait = self.setup_fluent_wait(driver)
        
        # 等待元素出现并可交互
        element = wait.until(
            lambda d: d.find_element(By.ID, "dynamic-button")
        )
        
        # 等待元素变为可点击
        element = wait.until(
            EC.element_to_be_clickable((By.ID, "dynamic-button"))
        )
        
        return element
    
    def advanced_fluent_example(self, driver):
        """高级Fluent Wait示例"""
        
        # 配置Fluent Wait
        wait = WebDriverWait(
            driver,
            timeout=20,
            poll_frequency=0.2,  # 每200ms轮询一次
            ignored_exceptions=[
                NoSuchElementException,
                ElementNotInteractableException
            ]
        )
        
        # 复杂的等待条件
        def complex_condition(driver):
            """复杂等待条件"""
            try:
                # 检查多个条件
                element = driver.find_element(By.ID, "status")
                if element.is_displayed() and element.is_enabled():
                    text = element.text
                    return "完成" in text or "成功" in text
            except:
                pass
            return False
        
        # 等待复杂条件满足
        result = wait.until(complex_condition)
        
        # 或者在等待时执行操作
        result = wait.until(
            lambda d: d.find_element(By.ID, "input-field").send_keys("test") or True
        )
        
        return result

3.7 等待策略最佳实践

python 复制代码
class WaitStrategyGuide:
    """等待策略最佳实践指南"""
    
    @staticmethod
    def best_practices():
        """等待策略最佳实践"""
        
        practices = {
            "通用原则": [
                " 优先使用显式等待,避免强制等待",
                " 不要混合使用隐式等待和显式等待",
                " 设置合理的超时时间(通常5-10秒)",
                " 针对关键元素使用特定等待条件"
            ],
            "页面加载等待": [
                "使用: driver.get() 和 driver.navigate().to()",
                "说明: Selenium默认等待页面加载完成(readyState=complete)",
                "扩展: 可配置pageLoadStrategy为'eager'或'none'"
            ],
            "元素存在等待": [
                "使用: EC.presence_of_element_located()",
                "适用: 元素已在DOM中,但可能未显示",
                "示例: 动态添加到页面的元素"
            ],
            "元素可见等待": [
                "使用: EC.visibility_of_element_located()",
                "适用: 需要与元素交互(点击、输入)",
                "示例: 弹出框、加载完成后显示的内容"
            ],
            "元素可点击等待": [
                "使用: EC.element_to_be_clickable()",
                "适用: 点击按钮、链接等",
                "示例: 提交按钮、导航链接"
            ],
            "元素消失等待": [
                "使用: EC.invisibility_of_element_located()",
                "适用: 等待加载动画消失",
                "示例: loading spinner、进度条"
            ]
        }
        
        return practices
    
    @staticmethod
    def wait_decision_tree():
        """等待策略决策树"""
        
        return """
        ┌─────────────────────┐
        │   开始等待决策      │
        └──────────┬──────────┘
                   ↓
        ┌─────────────────────┐
        │   是否知道具体元素   │
        └──────────┬──────────┘
           是↓             ↓否
        ┌──────────┐  ┌──────────┐
        │显式等待  │  │隐式等待  │
        │精准控制  │  │通用兜底  │
        └────┬─────┘  └────┬─────┘
             ↓              ↓
        ┌─────────────────────┐
        │    等待什么条件?    │
        └──────────┬──────────┘
        ↓           ↓           ↓
    ┌───────┐  ┌───────┐  ┌───────┐
    │存在性  │  │可见性  │  │可点击性│
    │presence│  │visibility│  │clickable│
    └───────┘  └───────┘  └───────┘
        """
    
    @staticmethod
    def wait_code_template():
        """等待代码模板"""
        
        return '''
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By

class PageObject:
    """页面对象基类"""
    
    def __init__(self, driver):
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)  # 默认10秒超时
    
    def wait_for_element(self, locator, condition="visible", timeout=None):
        """
        等待元素的核心方法
        
        Args:
            locator: (By.ID, "element-id") 格式的定位器
            condition: 等待条件 (presence/visible/clickable)
            timeout: 自定义超时时间
        """
        wait = WebDriverWait(self.driver, timeout or 10)
        
        conditions = {
            "presence": EC.presence_of_element_located,
            "visible": EC.visibility_of_element_located,
            "clickable": EC.element_to_be_clickable
        }
        
        if condition in conditions:
            return wait.until(conditions[condition](locator))
        else:
            raise ValueError(f"不支持的等待条件: {condition}")
    
    def wait_for_text(self, locator, text, timeout=None):
        """等待元素包含特定文本"""
        wait = WebDriverWait(self.driver, timeout or 10)
        return wait.until(
            EC.text_to_be_present_in_element(locator, text)
        )
    
    def wait_for_disappear(self, locator, timeout=None):
        """等待元素消失"""
        wait = WebDriverWait(self.driver, timeout or 10)
        return wait.until(
            EC.invisibility_of_element_located(locator)
        )
'''

四、框架封装:从脚本到工程

4.1 为什么要封装?

原始Selenium脚本存在以下问题:

  • 代码重复:每个测试都要写相同的find_element、等待逻辑
  • 维护困难:元素定位器散落在各个测试中
  • 可读性差:测试逻辑被技术细节淹没
  • 不稳定:缺乏统一的异常处理

4.2 基础层封装:BasePage

python 复制代码
"""
base_page.py - 页面基类
对所有Selenium操作进行二次封装,提供统一的异常处理和日志记录
"""
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import TimeoutException, NoSuchElementException
import logging
import time
from pathlib import Path

class BasePage:
    """
    页面基类,所有页面对象都应继承此类
    
    提供统一的元素操作方法和等待机制,封装了Selenium的底层细节
    """
    
    def __init__(self, driver):
        """
        初始化页面基类
        
        Args:
            driver: WebDriver实例
        """
        self.driver = driver
        self.wait = WebDriverWait(driver, 10)
        self.logger = logging.getLogger(self.__class__.__name__)
        
    # ==================== 元素定位基础方法 ====================
    
    def find_element(self, locator, timeout=10):
        """
        查找单个元素,带显式等待
        
        Args:
            locator: (By.ID, "element-id") 格式的定位器
            timeout: 超时时间
            
        Returns:
            WebElement: 找到的元素
        """
        try:
            wait = WebDriverWait(self.driver, timeout)
            element = wait.until(EC.presence_of_element_located(locator))
            self.logger.debug(f"元素定位成功: {locator}")
            return element
        except TimeoutException:
            self.logger.error(f"元素定位超时: {locator}")
            self.take_screenshot("element_timeout")
            raise
    
    def find_elements(self, locator, timeout=10):
        """
        查找多个元素
        
        Args:
            locator: 定位器
            timeout: 超时时间
        """
        try:
            wait = WebDriverWait(self.driver, timeout)
            elements = wait.until(EC.presence_of_all_elements_located(locator))
            self.logger.debug(f"找到 {len(elements)} 个元素: {locator}")
            return elements
        except TimeoutException:
            self.logger.error(f"元素列表定位超时: {locator}")
            return []
    
    # ==================== 等待条件封装 ====================
    
    def wait_element_visible(self, locator, timeout=10):
        """等待元素可见"""
        try:
            element = self.wait.until(EC.visibility_of_element_located(locator))
            self.logger.debug(f"元素可见: {locator}")
            return element
        except TimeoutException:
            self.logger.error(f"元素不可见: {locator}")
            self.take_screenshot("element_not_visible")
            raise
    
    def wait_element_clickable(self, locator, timeout=10):
        """等待元素可点击"""
        try:
            element = self.wait.until(EC.element_to_be_clickable(locator))
            self.logger.debug(f"元素可点击: {locator}")
            return element
        except TimeoutException:
            self.logger.error(f"元素不可点击: {locator}")
            self.take_screenshot("element_not_clickable")
            raise
    
    def wait_element_disappear(self, locator, timeout=10):
        """等待元素消失"""
        try:
            result = self.wait.until(EC.invisibility_of_element_located(locator))
            self.logger.debug(f"元素消失: {locator}")
            return result
        except TimeoutException:
            self.logger.error(f"元素未消失: {locator}")
            return False
    
    def wait_for_text(self, locator, text, timeout=10):
        """等待元素包含特定文本"""
        try:
            result = self.wait.until(
                EC.text_to_be_present_in_element(locator, text)
            )
            self.logger.debug(f"元素包含文本 '{text}': {locator}")
            return result
        except TimeoutException:
            self.logger.error(f"元素未包含文本 '{text}': {locator}")
            return False
    
    # ==================== 基础操作封装 ====================
    
    def click(self, locator, timeout=10):
        """
        点击元素
        
        Args:
            locator: 元素定位器
            timeout: 等待时间
        """
        element = self.wait_element_clickable(locator, timeout)
        try:
            element.click()
            self.logger.info(f"点击元素: {locator}")
        except Exception as e:
            self.logger.error(f"点击元素失败: {locator}, 错误: {str(e)}")
            self.take_screenshot("click_error")
            raise
    
    def input_text(self, locator, text, timeout=10):
        """
        输入文本
        
        Args:
            locator: 元素定位器
            text: 要输入的文本
            timeout: 等待时间
        """
        element = self.wait_element_visible(locator, timeout)
        try:
            element.clear()
            element.send_keys(text)
            self.logger.info(f"输入文本 '{text}' 到元素: {locator}")
        except Exception as e:
            self.logger.error(f"输入文本失败: {locator}, 错误: {str(e)}")
            self.take_screenshot("input_error")
            raise
    
    def get_text(self, locator, timeout=10):
        """获取元素文本"""
        element = self.wait_element_visible(locator, timeout)
        text = element.text
        self.logger.debug(f"获取元素文本 '{text}': {locator}")
        return text
    
    def get_attribute(self, locator, attribute, timeout=10):
        """获取元素属性"""
        element = self.find_element(locator, timeout)
        value = element.get_attribute(attribute)
        self.logger.debug(f"获取元素属性 {attribute}='{value}': {locator}")
        return value
    
    # ==================== 高级操作封装 ====================
    
    def select_by_text(self, locator, text, timeout=10):
        """根据文本选择下拉选项"""
        element = self.find_element(locator, timeout)
        select = Select(element)
        select.select_by_visible_text(text)
        self.logger.info(f"选择下拉选项 '{text}': {locator}")
    
    def select_by_value(self, locator, value, timeout=10):
        """根据值选择下拉选项"""
        element = self.find_element(locator, timeout)
        select = Select(element)
        select.select_by_value(value)
        self.logger.info(f"选择下拉值 '{value}': {locator}")
    
    def select_by_index(self, locator, index, timeout=10):
        """根据索引选择下拉选项"""
        element = self.find_element(locator, timeout)
        select = Select(element)
        select.select_by_index(index)
        self.logger.info(f"选择下拉索引 '{index}': {locator}")
    
    def hover(self, locator, timeout=10):
        """鼠标悬停"""
        element = self.find_element(locator, timeout)
        actions = ActionChains(self.driver)
        actions.move_to_element(element).perform()
        self.logger.info(f"鼠标悬停: {locator}")
    
    def drag_and_drop(self, source_locator, target_locator, timeout=10):
        """拖拽元素"""
        source = self.find_element(source_locator, timeout)
        target = self.find_element(target_locator, timeout)
        actions = ActionChains(self.driver)
        actions.drag_and_drop(source, target).perform()
        self.logger.info(f"拖拽元素: {source_locator} -> {target_locator}")
    
    def press_enter(self, locator, timeout=10):
        """在元素上按回车"""
        element = self.find_element(locator, timeout)
        element.send_keys(Keys.RETURN)
        self.logger.info(f"按回车键: {locator}")
    
    def scroll_to_element(self, locator, timeout=10):
        """滚动到元素位置"""
        element = self.find_element(locator, timeout)
        self.driver.execute_script("arguments[0].scrollIntoView(true);", element)
        self.logger.info(f"滚动到元素: {locator}")
    
    def scroll_to_bottom(self):
        """滚动到页面底部"""
        self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
        self.logger.info("滚动到页面底部")
    
    # ==================== 弹窗处理 ====================
    
    def accept_alert(self, timeout=10):
        """接受弹窗"""
        try:
            alert = self.wait.until(EC.alert_is_present())
            alert.accept()
            self.logger.info("接受弹窗")
        except TimeoutException:
            self.logger.warning("弹窗未出现")
    
    def dismiss_alert(self, timeout=10):
        """取消弹窗"""
        try:
            alert = self.wait.until(EC.alert_is_present())
            alert.dismiss()
            self.logger.info("取消弹窗")
        except TimeoutException:
            self.logger.warning("弹窗未出现")
    
    def get_alert_text(self, timeout=10):
        """获取弹窗文本"""
        try:
            alert = self.wait.until(EC.alert_is_present())
            text = alert.text
            self.logger.info(f"弹窗文本: {text}")
            return text
        except TimeoutException:
            self.logger.warning("弹窗未出现")
            return None
    
    # ==================== iframe处理 ====================
    
    def switch_to_frame(self, locator, timeout=10):
        """切换到iframe"""
        try:
            frame = self.wait.until(EC.frame_to_be_available_and_switch_to_it(locator))
            self.logger.info(f"切换到iframe: {locator}")
            return frame
        except TimeoutException:
            self.logger.error(f"iframe不可用: {locator}")
            raise
    
    def switch_to_default_content(self):
        """切回主文档"""
        self.driver.switch_to.default_content()
        self.logger.info("切回主文档")
    
    # ==================== 辅助方法 ====================
    
    def take_screenshot(self, name=None):
        """截图"""
        if name is None:
            name = f"screenshot_{int(time.time())}"
        
        screenshot_dir = Path("screenshots")
        screenshot_dir.mkdir(exist_ok=True)
        
        filepath = screenshot_dir / f"{name}.png"
        self.driver.save_screenshot(str(filepath))
        self.logger.info(f"截图已保存: {filepath}")
        return str(filepath)
    
    def execute_script(self, script, *args):
        """执行JavaScript"""
        result = self.driver.execute_script(script, *args)
        self.logger.debug(f"执行JS: {script[:50]}...")
        return result
    
    def is_element_present(self, locator, timeout=5):
        """判断元素是否存在"""
        try:
            self.find_element(locator, timeout)
            return True
        except:
            return False
    
    def is_element_visible(self, locator, timeout=5):
        """判断元素是否可见"""
        try:
            self.wait_element_visible(locator, timeout)
            return True
        except:
            return False

4.3 页面对象层:LoginPage

python 复制代码
"""
login_page.py - 登录页面对象
继承BasePage,实现具体的页面操作
"""
from selenium.webdriver.common.by import By
from base_page import BasePage

class LoginPage(BasePage):
    """
    登录页面对象模型
    
    封装登录页面的所有元素和操作,测试用例通过调用这些方法与页面交互
    """
    
    # ==================== 元素定位器 ====================
    # 集中管理所有定位器,便于维护
    
    USERNAME_INPUT = (By.ID, "username")
    PASSWORD_INPUT = (By.ID, "password")
    LOGIN_BUTTON = (By.ID, "loginBtn")
    ERROR_MESSAGE = (By.CLASS_NAME, "error-message")
    REMEMBER_ME_CHECKBOX = (By.ID, "rememberMe")
    FORGOT_PASSWORD_LINK = (By.LINK_TEXT, "Forgot Password?")
    REGISTER_LINK = (By.PARTIAL_LINK_TEXT, "Register")
    CAPTCHA_IMAGE = (By.CSS_SELECTOR, "img.captcha")
    
    def __init__(self, driver):
        """初始化登录页面"""
        super().__init__(driver)
        self.url = "https://example.com/login"
    
    # ==================== 页面操作 ====================
    
    def open(self):
        """打开登录页面"""
        self.driver.get(self.url)
        self.logger.info(f"打开登录页面: {self.url}")
        # 等待页面加载完成
        self.wait_element_visible(self.USERNAME_INPUT)
        return self
    
    def login(self, username, password, remember_me=False):
        """
        执行登录操作
        
        Args:
            username: 用户名
            password: 密码
            remember_me: 是否记住我
        """
        self.logger.info(f"尝试登录: username={username}, remember_me={remember_me}")
        
        # 输入用户名
        self.input_text(self.USERNAME_INPUT, username)
        
        # 输入密码
        self.input_text(self.PASSWORD_INPUT, password)
        
        # 是否勾选记住我
        if remember_me:
            self.click(self.REMEMBER_ME_CHECKBOX)
        
        # 点击登录按钮
        self.click(self.LOGIN_BUTTON)
        
        # 返回Dashboard页面对象
        from dashboard_page import DashboardPage
        return DashboardPage(self.driver)
    
    def login_with_enter_key(self, username, password):
        """使用回车键登录"""
        self.input_text(self.USERNAME_INPUT, username)
        self.input_text(self.PASSWORD_INPUT, password)
        self.press_enter(self.PASSWORD_INPUT)
        
        from dashboard_page import DashboardPage
        return DashboardPage(self.driver)
    
    def get_error_message(self):
        """获取登录错误信息"""
        if self.is_element_visible(self.ERROR_MESSAGE):
            return self.get_text(self.ERROR_MESSAGE)
        return None
    
    def is_login_button_enabled(self):
        """检查登录按钮是否可用"""
        element = self.find_element(self.LOGIN_BUTTON)
        return element.is_enabled()
    
    def click_forgot_password(self):
        """点击忘记密码链接"""
        self.click(self.FORGOT_PASSWORD_LINK)
        # 可能返回忘记密码页面对象
    
    def click_register(self):
        """点击注册链接"""
        self.click(self.REGISTER_LINK)
        # 可能返回注册页面对象
    
    # ==================== 验证方法 ====================
    
    def is_at(self):
        """验证是否在登录页面"""
        return self.is_element_visible(self.USERNAME_INPUT)
    
    def get_page_title(self):
        """获取页面标题"""
        return self.driver.title
    
    def get_login_button_text(self):
        """获取登录按钮文本"""
        return self.get_attribute(self.LOGIN_BUTTON, "value")

4.4 DashboardPage

python 复制代码
"""
dashboard_page.py - 仪表盘页面对象
"""
from selenium.webdriver.common.by import By
from base_page import BasePage

class DashboardPage(BasePage):
    """仪表盘页面对象"""
    
    # 元素定位器
    WELCOME_MESSAGE = (By.CSS_SELECTOR, ".welcome-message")
    USER_AVATAR = (By.CLASS_NAME, "avatar")
    LOGOUT_BUTTON = (By.ID, "logoutBtn")
    NAVIGATION_MENU = (By.CSS_SELECTOR, "nav.main-menu")
    STATS_CARDS = (By.CLASS_NAME, "stats-card")
    
    def __init__(self, driver):
        super().__init__(driver)
        # 等待页面加载完成
        self.wait_element_visible(self.WELCOME_MESSAGE)
    
    def get_welcome_message(self):
        """获取欢迎信息"""
        return self.get_text(self.WELCOME_MESSAGE)
    
    def logout(self):
        """登出系统"""
        self.click(self.LOGOUT_BUTTON)
        from login_page import LoginPage
        return LoginPage(self.driver)
    
    def get_stats_count(self):
        """获取统计卡片数量"""
        cards = self.find_elements(self.STATS_CARDS)
        return len(cards)
    
    def is_user_logged_in(self):
        """验证用户是否已登录"""
        return self.is_element_visible(self.USER_AVATAR)

4.5 测试用例层

python 复制代码
"""
test_login.py - 登录功能测试用例
使用Page Object编写清晰、可维护的测试
"""
import pytest
from login_page import LoginPage
from dashboard_page import DashboardPage

class TestLogin:
    """登录功能测试类"""
    
    @pytest.fixture(autouse=True)
    def setup(self, driver):
        """测试前置条件"""
        self.driver = driver
        self.login_page = LoginPage(driver).open()
        yield
        # 测试后清理
        self.driver.delete_all_cookies()
    
    def test_valid_login(self):
        """测试有效登录"""
        # 执行登录操作
        dashboard = self.login_page.login("admin", "admin123")
        
        # 验证登录成功
        assert dashboard.is_user_logged_in()
        assert "Welcome" in dashboard.get_welcome_message()
    
    def test_invalid_password_login(self):
        """测试无效密码登录"""
        self.login_page.login("admin", "wrong_password")
        
        # 验证错误提示
        error = self.login_page.get_error_message()
        assert "Invalid username or password" in error
        assert self.login_page.is_at()  # 仍在登录页面
    
    def test_empty_username_login(self):
        """测试空用户名登录"""
        self.login_page.input_text(self.login_page.PASSWORD_INPUT, "admin123")
        
        # 验证登录按钮状态
        assert not self.login_page.is_login_button_enabled()
    
    def test_remember_me_functionality(self):
        """测试记住我功能"""
        self.login_page.login("admin", "admin123", remember_me=True)
        
        # 登出
        dashboard = DashboardPage(self.driver)
        dashboard.logout()
        
        # 重新打开登录页,验证用户名是否记住
        self.login_page.open()
        remembered_username = self.login_page.get_attribute(
            self.login_page.USERNAME_INPUT, "value"
        )
        assert remembered_username == "admin"
    
    @pytest.mark.parametrize("username,password,expected_error", [
        ("", "admin123", "Username is required"),
        ("admin", "", "Password is required"),
        ("", "", "Username and password are required"),
    ])
    def test_login_validation(self, username, password, expected_error):
        """参数化测试表单验证"""
        if username:
            self.login_page.input_text(self.login_page.USERNAME_INPUT, username)
        if password:
            self.login_page.input_text(self.login_page.PASSWORD_INPUT, password)
        
        # 验证错误提示
        if not username or not password:
            assert not self.login_page.is_login_button_enabled()
        else:
            self.login_page.click(self.login_page.LOGIN_BUTTON)
            error = self.login_page.get_error_message()
            assert expected_error in error

4.6 配置与工具类

python 复制代码
"""
config.py - 配置文件
"""
import os
from dotenv import load_dotenv

# 加载环境变量
load_dotenv()

class Config:
    """全局配置"""
    
    # 测试环境
    ENV = os.getenv("TEST_ENV", "staging")
    
    # URL配置
    BASE_URL = os.getenv("BASE_URL", "https://example.com")
    
    # 浏览器配置
    BROWSER = os.getenv("BROWSER", "chrome")
    HEADLESS = os.getenv("HEADLESS", "false").lower() == "true"
    
    # 超时配置
    DEFAULT_TIMEOUT = int(os.getenv("DEFAULT_TIMEOUT", "10"))
    PAGE_LOAD_TIMEOUT = int(os.getenv("PAGE_LOAD_TIMEOUT", "30"))
    
    # 截图配置
    SCREENSHOT_ON_FAILURE = os.getenv("SCREENSHOT_ON_FAILURE", "true").lower() == "true"
    SCREENSHOT_DIR = "reports/screenshots"
python 复制代码
"""
driver_factory.py - WebDriver工厂
"""
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from webdriver_manager.chrome import ChromeDriverManager
from webdriver_manager.firefox import GeckoDriverManager
from config import Config
import logging

class DriverFactory:
    """WebDriver工厂类,负责创建和配置浏览器驱动"""
    
    @staticmethod
    def create_driver(browser=None):
        """
        创建WebDriver实例
        
        Args:
            browser: 浏览器类型,默认使用配置
        """
        browser = browser or Config.BROWSER
        logger = logging.getLogger(__name__)
        
        if browser.lower() == "chrome":
            options = webdriver.ChromeOptions()
            
            if Config.HEADLESS:
                options.add_argument("--headless")
                options.add_argument("--no-sandbox")
                options.add_argument("--disable-dev-shm-usage")
            
            options.add_argument("--disable-gpu")
            options.add_argument("--window-size=1920,1080")
            options.add_argument("--disable-blink-features=AutomationControlled")
            
            service = Service(ChromeDriverManager().install())
            driver = webdriver.Chrome(service=service, options=options)
            logger.info("Chrome驱动初始化成功")
            
        elif browser.lower() == "firefox":
            options = webdriver.FirefoxOptions()
            
            if Config.HEADLESS:
                options.add_argument("--headless")
            
            service = Service(GeckoDriverManager().install())
            driver = webdriver.Firefox(service=service, options=options)
            logger.info("Firefox驱动初始化成功")
            
        else:
            raise ValueError(f"不支持的浏览器: {browser}")
        
        # 设置超时
        driver.set_page_load_timeout(Config.PAGE_LOAD_TIMEOUT)
        driver.implicitly_wait(Config.DEFAULT_TIMEOUT)
        driver.maximize_window()
        
        return driver
python 复制代码
"""
conftest.py - pytest配置
"""
import pytest
from driver_factory import DriverFactory
from config import Config
import logging

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

@pytest.fixture(scope="function")
def driver():
    """浏览器驱动fixture,每个测试函数独立使用"""
    driver = DriverFactory.create_driver()
    yield driver
    driver.quit()

@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 and Config.SCREENSHOT_ON_FAILURE:
        if "driver" in item.funcargs:
            driver = item.funcargs["driver"]
            test_name = item.name
            screenshot_path = f"{Config.SCREENSHOT_DIR}/{test_name}.png"
            driver.save_screenshot(screenshot_path)
            print(f"\n测试失败,截图已保存: {screenshot_path}")

五、实战演练:完整的测试流程

5.1 测试场景:电商网站登录-搜索-下单流程

python 复制代码
"""
test_e2e_purchase.py - 端到端购买流程测试
"""
import pytest
from login_page import LoginPage
from search_page import SearchPage
from product_page import ProductPage
from cart_page import CartPage
from checkout_page import CheckoutPage

class TestPurchaseFlow:
    """完整购买流程测试"""
    
    def test_complete_purchase_flow(self, driver):
        """测试完整购买流程"""
        
        # 1. 登录
        login_page = LoginPage(driver).open()
        dashboard = login_page.login("test_user", "Test@123")
        assert dashboard.is_user_logged_in()
        
        # 2. 搜索商品
        search_page = SearchPage(driver)
        search_page.search("iPhone 15")
        results = search_page.get_search_results()
        assert len(results) > 0
        
        # 3. 选择商品
        product_page = search_page.click_first_result()
        product_name = product_page.get_product_name()
        product_price = product_page.get_price()
        
        # 4. 加入购物车
        product_page.add_to_cart()
        assert product_page.get_cart_count() == "1"
        
        # 5. 查看购物车
        cart_page = product_page.go_to_cart()
        assert product_name in cart_page.get_cart_items()
        assert cart_page.get_total_price() == product_price
        
        # 6. 结算
        checkout_page = cart_page.proceed_to_checkout()
        checkout_page.fill_shipping_info({
            "name": "Test User",
            "address": "123 Test St",
            "phone": "1234567890"
        })
        
        # 7. 确认订单
        order_page = checkout_page.place_order()
        assert "Order Confirmed" in order_page.get_confirmation_message()
        assert order_page.get_order_number() is not None

六、最佳实践与常见问题

6.1 元素定位最佳实践

实践 说明
优先使用ID ID在页面中唯一,定位最快最稳定
CSS选择器优于XPath CSS选择器性能更好,可读性更强
避免绝对路径 绝对XPath脆弱,页面结构变化就会失效
使用相对路径 基于元素特征定位,更稳定
定位器集中管理 将定位器定义为类常量,便于维护
优先使用可读性好的定位器 让团队成员能理解定位的是什么元素

6.2 等待策略最佳实践

场景 推荐方案
页面加载 配置pageLoadStrategy,或等待特定元素出现
元素出现 EC.presence_of_element_located
元素可点击 EC.element_to_be_clickable
元素可见 EC.visibility_of_element_located
元素消失 EC.invisibility_of_element_located
弹窗出现 EC.alert_is_present
iframe可用 EC.frame_to_be_available_and_switch_to_it
标题变化 EC.title_contains

6.3 框架设计最佳实践

实践 说明
使用Page Object模式 封装页面细节,测试用例更清晰
继承BasePage 公共方法放在基类,避免重复代码
集中管理定位器 所有定位器定义为类常量,便于维护
数据驱动测试 测试数据与脚本分离
参数化测试 使用pytest.mark.parametrize
自动截图 失败时自动截图,便于调试
日志记录 记录关键操作,便于排查问题

6.4 常见问题与解决方案

问题1:元素定位失败
python 复制代码
# 原因:页面未加载完成、元素在iframe中、元素动态变化

# 解决方案:
# 1. 使用显式等待
element = WebDriverWait(driver, 10).until(
    EC.presence_of_element_located((By.ID, "dynamic-element"))
)

# 2. 检查是否在iframe中
driver.switch_to.frame("frame-name")

# 3. 使用更灵活的定位器(如包含动态部分)
element = driver.find_element(By.CSS_SELECTOR, "div[id^='temp_']")
问题2:元素不可交互
python 复制代码
# 原因:元素被遮挡、不可见、禁用

# 解决方案:
# 1. 等待元素可点击
element = WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.ID, "button"))
)

# 2. 使用JavaScript强制点击
driver.execute_script("arguments[0].click();", element)

# 3. 先滚动到元素位置
driver.execute_script("arguments[0].scrollIntoView(true);", element)
问题3:测试不稳定
python 复制代码
# 原因:网络延迟、服务器响应慢、资源加载

# 解决方案:
# 1. 使用合理的等待策略,避免固定等待
# 2. 添加失败重试机制
@pytest.mark.flaky(reruns=3, reruns_delay=2)
def test_flaky_scenario():
    # 测试代码
    pass

# 3. 隔离测试,避免依赖
# 4. 使用稳定环境(如测试专用服务器)

结语:从脚本到工程,从自动化到智能化

Selenium是Web自动化测试的基石,但掌握Selenium本身不是终点。真正的价值在于:

  1. 解决实际问题:提高回归测试效率,保障产品质量
  2. 工程化思维:从零散脚本到可维护框架
  3. 持续优化:根据反馈不断改进测试策略
  4. 拥抱新技术:关注AI测试、视觉测试等新方向

下一篇文章预告 :接口测试全攻略:Postman+Newman实现API自动化

我们将深入接口测试的世界,学习如何使用Postman设计API测试,通过Newman集成到CI/CD流水线,实现从UI到接口的全方位自动化覆盖。


附录:完整项目结构

复制代码
selenium-framework/
├── src/
│   ├── pages/              # 页面对象
│   │   ├── base_page.py
│   │   ├── login_page.py
│   │   ├── dashboard_page.py
│   │   └── ...
│   ├── tests/               # 测试用例
│   │   ├── test_login.py
│   │   ├── test_search.py
│   │   └── ...
│   ├── utils/               # 工具类
│   │   ├── driver_factory.py
│   │   ├── logger.py
│   │   └── ...
│   └── config/              # 配置文件
│       ├── config.py
│       └── .env
├── reports/                  # 测试报告
│   ├── screenshots/
│   └── html/
├── test_data/                # 测试数据
│   ├── login_data.json
│   └── ...
├── logs/                      # 运行日志
├── conftest.py                # pytest配置
├── pytest.ini                 # pytest配置
├── requirements.txt           # 依赖
└── README.md                  # 项目说明

欢迎在评论区交流:

  1. 你在Selenium使用中遇到过哪些棘手的问题?
  2. 对于Page Object模式封装有什么心得?
  3. 期待在接口测试文章中看到哪些内容?

点赞 + 收藏 + 关注,不错过后续14篇干货更新!

相关推荐
测试老哥2 小时前
如何使用Postman做接口测试?
自动化测试·软件测试·python·测试工具·测试用例·接口测试·postman
安全不再安全3 小时前
某驱动任意读漏洞分析 - 可用于游戏内存数据读取
c语言·测试工具·安全·游戏·网络安全
网络安全-老纪15 小时前
一文2000字手把手教你自动化测试Selenium+pytest+数据驱动
自动化测试·软件测试·selenium·测试工具·pytest
可可南木20 小时前
3070文件格式--17--设备文件
功能测试·测试工具·pcb工艺
weixin_440730501 天前
05接口测试-01接口理论+02posman的使用
功能测试·测试工具·postman
我的xiaodoujiao1 天前
使用 Python 语言 从 0 到 1 搭建完整 Web UI自动化测试学习系列 51--CI/CD 4--推送本地代码到Git远程仓库
python·学习·测试工具·ci/cd·pytest
派大星-?2 天前
自动化测试五模块一框架(上)
开发语言·python·测试工具·单元测试·可用性测试
没有bug.的程序员3 天前
自动化测试之魂:Selenium 与 TestNG 深度集成内核、Page Object 模型实战与 Web UI 交付质量指南
前端·自动化测试·selenium·ui·testng·page·object