Selenium+pytest 页面对象模型框架

下载地址:https://gitee.com/xiaopo1998/web_ui_test.git

Selenium 页面对象模型框架使用说明

本框架基于 Selenium WebDriver 实现了一套模块化、易维护的页面对象模型(Page Object Model)架构,将不同类型的操作按职责分离到不同的类中。

框架结构

框架采用分层设计,由以下主要组件组成:

core/

├── base_page.py # 基础页面类,包含核心操作方法及其他组件的访问

├── element_actions.py # 元素等待和检查操作

├── action_chains.py # 鼠标和键盘高级操作

├── browser_operations.py # 窗口、框架和警告框操作

├── browser_utils.py # Cookie、JavaScript和截图等辅助功能

├── select_operations.py # 下拉框专用操作

├── webdriver.py # WebDriver管理

└── logger.py # 日志管理

组件说明

  1. BasePage (base_page.py)
    基础页面类是整个框架的核心,负责提供基本的页面操作方法,并作为访问其他专用组件的入口点。它包含以下核心功能:

提供基本的元素查找方法

提供常用的元素操作方法(点击、输入、获取文本等)

负责实例化和提供对所有专用组件的访问

  1. ElementActions (element_actions.py)

元素等待和检查操作类,专门负责处理元素的可见性、存在性等检查和等待操作。主要功能包括:

提供各种元素等待方法(等待可见、等待可点击等)

提供元素状态检查方法(是否存在、是否可见等)

提供文本和属性相关的等待方法

  1. ActionChainOperations (action_chains.py)

鼠标和键盘高级操作类,负责处理复杂的交互操作。主要功能包括:

鼠标操作(悬停、拖放、右键点击等)

键盘操作(按键、组合键等)

其他高级交互操作

  1. BrowserOperations (browser_operations.py)

浏览器操作类,负责处理窗口、框架和警告框等操作。主要功能包括:

窗口操作(切换、关闭、获取句柄等)

框架操作(切换框架、返回主文档等)

警告框操作(接受、取消、获取文本等)

页面导航(前进、后退、刷新等)

  1. BrowserUtils (browser_utils.py)

浏览器工具类,提供 Cookie 管理、JavaScript 执行和截图等辅助功能。主要功能包括:

Cookie 管理(获取、添加、删除等)

JavaScript 执行(滚动页面等)

截图功能(全页面截图、元素截图等)

  1. SelectOperations (select_operations.py)

下拉框操作类,专门处理下拉框元素的操作。主要功能包括:

选择选项(通过文本、值、索引等)

获取选中选项信息

多选下拉框操作(选择多个、取消选择等)

使用方法

初始化

首先需要初始化 BasePage 对象,它是框架的核心入口点:

from selenium import webdriver

from core.base_page import BasePage

初始化WebDriver

driver = webdriver.Chrome()

创建BasePage实例

page = BasePage(driver)

导航到页面

page.navigate_to("https://example.com")

基本元素操作

BasePage 类提供了最基本的元素操作方法:

定义元素定位器

from selenium.webdriver.common.by import By

username_locator = (By.ID, "username")

password_locator = (By.ID, "password")

login_button_locator = (By.ID, "login")

查找元素

username_element = page.find_element(username_locator)

基本元素操作

page.input_text(username_locator, "test_user")

page.input_text(password_locator, "password123")

page.click(login_button_locator)

获取元素文本

text = page.get_text((By.ID, "welcome_message"))

print(text)

获取元素属性

value = page.get_attribute((By.ID, "username"), "value")

元素等待与检查

通过 element 属性访问元素等待和检查功能:

等待元素可见

page.element.wait_for_element_visible((By.ID, "dashboard"))

检查元素是否存在

if page.element.is_element_present((By.ID, "error_message")):

print("登录失败")

等待文本出现

page.element.wait_for_text_present((By.ID, "status"), "成功")

高亮显示元素(调试时很有用)

page.element.highlight_element((By.LINK_TEXT, "用户设置"))

等待元素不可见

page.element.wait_for_element_not_visible((By.ID, "loading"))

滚动到元素位置

page.element.scroll_to_element((By.ID, "footer"))

鼠标和键盘操作

通过 actions 属性访问鼠标和键盘高级操作:

from selenium.webdriver.common.keys import Keys

鼠标悬停

page.actions.mouse_hover((By.ID, "menu"))

右键点击

page.actions.right_click((By.ID, "context_menu"))

双击

page.actions.double_click((By.ID, "double_click_element"))

拖放操作

page.actions.drag_and_drop((By.ID, "source"), (By.ID, "target"))

按键操作

page.actions.press_key((By.ID, "search_box"), Keys.ENTER)

组合键

page.actions.press_key_sequence((By.ID, "text_editor"), Keys.CONTROL, "a")

点击并按住

page.actions.click_and_hold((By.ID, "slider"))

释放

page.actions.release()

移动鼠标

page.actions.move_by_offset(10, 20)

浏览器窗口和框架操作

通过 browser 属性访问窗口、框架和警告框操作:

窗口操作

original_handle = page.browser.get_current_window_handle()

page.browser.open_new_tab("https://example.com/other_page")

page.browser.switch_to_window(window_index=0) # 切回原窗口

page.browser.close_current_window()

框架操作

page.browser.switch_to_frame((By.ID, "iframe_id"))

page.browser.switch_to_default_content()

page.browser.switch_to_parent_frame()

警告框操作

page.browser.accept_alert()

alert_text = page.browser.get_alert_text()

page.browser.dismiss_alert()

page.browser.send_text_to_alert("输入内容")

导航操作

page.browser.refresh_page()

page.browser.go_back()

page.browser.go_forward()

获取页面源码

html = page.browser.get_page_source()

等待页面加载

page.browser.wait_for_page_load()

Cookie和JavaScript操作

通过 utils 属性访问 Cookie、JavaScript 和截图功能:

Cookie操作

cookies = page.utils.get_cookies()

cookie = page.utils.get_cookie("session_id")

page.utils.add_cookie({"name": "session", "value": "123456"})

page.utils.delete_cookie("old_cookie")

page.utils.delete_all_cookies()

JavaScript操作

page.utils.scroll_to_top()

page.utils.scroll_to_bottom()

page.utils.scroll_by(0, 500) # 向下滚动500像素

截图

screenshot_path = page.utils.take_screenshot("login_page.png")

element_screenshot = page.utils.take_element_screenshot((By.ID, "error"), "error.png")

下拉框操作

通过 select 属性访问下拉框专用操作:

选择下拉框选项

page.select.select_by_text((By.ID, "country"), "中国")

page.select.select_by_value((By.ID, "country"), "CN")

page.select.select_by_index((By.ID, "country"), 0)

获取选中的选项

selected_text = page.select.get_selected_text((By.ID, "country"))

selected_value = page.select.get_selected_value((By.ID, "country"))

获取所有选项

all_options = page.select.get_all_options((By.ID, "country"))

all_option_texts = page.select.get_all_options_text((By.ID, "country"))

all_option_values = page.select.get_all_options_value((By.ID, "country"))

判断下拉框是否支持多选

is_multiple = page.select.is_multiple((By.ID, "multi_select"))

多选下拉框操作

if page.select.is_multiple((By.ID, "multi_select")):

page.select.select_by_text((By.ID, "multi_select"), "选项1")

page.select.select_by_text((By.ID, "multi_select"), "选项2")

page.select.deselect_by_text((By.ID, "multi_select"), "选项1")

page.select.deselect_all((By.ID, "multi_select"))

自定义页面类

在实际项目中,建议为每个页面创建一个继承自 BasePage 的子类:

from core.base_page import BasePage

from selenium.webdriver.common.by import By

class LoginPage(BasePage):

定义页面元素定位器

USERNAME_INPUT = (By.ID, "username")

PASSWORD_INPUT = (By.ID, "password")

LOGIN_BUTTON = (By.ID, "login")

ERROR_MESSAGE = (By.ID, "error")

复制代码
def login(self, username, password):
    """
    登录操作
    :param username: 用户名
    :param password: 密码
    """
    self.input_text(self.USERNAME_INPUT, username)
    self.input_text(self.PASSWORD_INPUT, password)
    self.click(self.LOGIN_BUTTON)
    
def get_error_message(self):
    """
    获取错误信息
    :return: 错误信息文本
    """
    if self.element.is_element_visible(self.ERROR_MESSAGE):
        return self.get_text(self.ERROR_MESSAGE)
    return None
    
def wait_for_login_success(self):
    """
    等待登录成功
    """
    self.element.wait_for_element_not_visible(self.ERROR_MESSAGE)
    self.browser.wait_for_page_load()

然后可以这样使用:

初始化页面

login_page = LoginPage(driver)

login_page.navigate_to("https://example.com/login")

执行登录

login_page.login("user123", "password123")

检查结果

error = login_page.get_error_message()

if error:

print(f"登录失败: {error}")

else:

print("登录成功")

最佳实践

按页面组织代码:为每个页面创建一个继承自 BasePage 的类

封装业务操作:将多个基本操作封装为有意义的业务方法

使用显式等待:始终使用等待方法确保元素就绪

保持定位器在页面类中集中定义:便于维护

使用适当的组件:根据操作类型选择合适的组件(element, actions, browser, utils, select)

采用合理的命名:方法名和变量名应当清晰地表明其作用

添加详细注释:对复杂操作添加注释说明

处理异常情况:捕获和处理可能的异常

异常处理

框架中的大多数方法都会记录日志并抛出异常(如 TimeoutException)。建议在测试代码中捕获这些异常:

try:

page.click((By.ID, "non_existent_button"))

except Exception as e:

print(f"操作失败: {e}")

page.utils.take_screenshot("error.png")

扩展框架

如需添加新功能,可以扩展现有类或创建新的操作类:

扩展ElementActions类

from core.element_actions import ElementActions

from selenium.webdriver.support.wait import WebDriverWait

class ExtendedElementActions(ElementActions):

def wait_for_element_count(self, locator, count, timeout=None):

"""

等待元素数量达到预期值

:param locator: 元素定位器

:param count: 期望的元素数量

:param timeout: 超时时间

"""

timeout = timeout or self.config['browser']['implicit_wait']

self.log.debug(f"等待元素 {locator} 数量达到 {count}")

复制代码
    def check_count(driver):
        elements = driver.find_elements(*locator)
        return len(elements) == count
        
    WebDriverWait(self.driver, timeout).until(check_count)

常见问题

元素找不到

确保使用正确的定位器

检查元素是否在页面中可见

考虑使用等待方法确保元素已加载

操作没有效果

检查元素是否被其他元素遮挡

确保元素处于可交互状态

尝试使用 JavaScript 执行操作

框架切换问题

操作完iframe中的元素后,记得切回默认内容

嵌套iframe需要逐层切换

多窗口操作

切换到新窗口后,如需返回原窗口,记得保存原窗口的句柄

注意关闭窗口后句柄列表会变化

元素选择问题

如果一个定位器匹配多个元素,find_element会返回第一个匹配的元素

可以使用更精确的定位策略或组合多个定位条件

更多示例

表单操作

加载表单页面

page.navigate_to("https://example.com/form")

输入文本

page.input_text((By.NAME, "first_name"), "张")

page.input_text((By.NAME, "last_name"), "三")

选择下拉框

page.select.select_by_text((By.NAME, "country"), "中国")

选择单选按钮

page.click((By.CSS_SELECTOR, "input[name='gender'][value='male']"))

勾选复选框

page.click((By.NAME, "agreement"))

提交表单

page.click((By.CSS_SELECTOR, "button[type='submit']"))

等待结果

page.element.wait_for_text_present((By.ID, "result"), "提交成功")

表格操作

获取表格所有行

rows = page.find_elements((By.CSS_SELECTOR, "table tr"))

遍历行

for row in rows[1:]: # 跳过表头

获取单元格

cells = row.find_elements_by_tag_name("td")

if len(cells) > 0:

print(f"第一列: {cells[0].text}, 第二列: {cells[1].text}")

复制代码
# 点击行中的按钮
if len(cells) > 3:
    edit_button = cells[3].find_element_by_tag_name("button")
    edit_button.click()
    break

文件上传

import os

上传文件

file_path = os.path.abspath("./test_file.txt")

page.input_text((By.ID, "file_input"), file_path)

点击上传按钮

page.click((By.ID, "upload_button"))

等待上传完成

page.element.wait_for_text_present((By.ID, "upload_status"), "上传成功")

测试用例示例

完整登录-搜索-购买流程

以下是一个完整的电商网站测试案例,展示如何将所有知识点整合起来:

import unittest

from selenium import webdriver

from selenium.common.exceptions import TimeoutException

from pages.login_page import LoginPage

from pages.home_page import HomePage

from pages.product_page import ProductPage

from pages.cart_page import CartPage

from pages.checkout_page import CheckoutPage

import time

class ShoppingTest(unittest.TestCase):

复制代码
def setUp(self):
    # 初始化WebDriver
    self.driver = webdriver.Chrome()
    self.driver.maximize_window()
    
    # 初始化所有页面对象
    self.login_page = LoginPage(self.driver)
    self.home_page = HomePage(self.driver)
    self.product_page = ProductPage(self.driver)
    self.cart_page = CartPage(self.driver)
    self.checkout_page = CheckoutPage(self.driver)
    
def tearDown(self):
    # 测试结束,关闭浏览器
    if self.driver:
        self.driver.quit()

@screenshot_on_failure  # 使用前面定义的装饰器
def test_purchase_flow(self):
    """测试完整购买流程"""
    # 测试数据
    username = "test_user"
    password = "Test@123"
    product_name = "测试商品"
    
    # 1. 登录
    self.login_page.navigate_to("https://example.com/login")
    self.login_page.wait_for_page_load()
    self.login_page.login(username, password)
    
    # 验证登录成功
    user_welcome = self.home_page.get_welcome_message()
    self.assertIn(username, user_welcome, "登录后欢迎信息未显示用户名")
    
    # 2. 搜索商品
    self.home_page.search_product(product_name)
    
    # 验证搜索结果
    search_count = self.home_page.get_search_results_count()
    self.assertGreater(search_count, 0, "搜索无结果")
    
    # 3. 打开商品详情
    self.home_page.click_first_product()
    
    # 验证商品详情页
    product_title = self.product_page.get_product_title()
    self.assertIn(product_name, product_title, f"商品详情页不匹配,期望包含'{product_name}'")
    
    # 4. 添加到购物车
    self.product_page.add_to_cart()
    
    # 5. 查看购物车
    self.product_page.go_to_cart()
    
    # 验证购物车
    cart_items = self.cart_page.get_cart_items()
    self.assertGreaterEqual(len(cart_items), 1, "购物车为空")
    
    # 6. 进入结账流程
    self.cart_page.proceed_to_checkout()
    
    # 7. 填写收货信息
    self.checkout_page.fill_shipping_info({
        "name": "张三",
        "phone": "13800138000",
        "address": "北京市海淀区某某街道",
        "zipcode": "100000"
    })
    
    # 8. 选择支付方式
    self.checkout_page.select_payment_method("cod")  # cash on delivery
    
    # 9. 提交订单
    order_number = self.checkout_page.place_order()
    
    # 验证订单提交成功
    success_message = self.checkout_page.get_success_message()
    self.assertIn("订单已成功提交", success_message)
    self.assertIsNotNone(order_number, "订单号未生成")
    
    # 10. 截取成功页面截图
    self.checkout_page.utils.take_screenshot(f"order_success_{order_number}.png")
    
    print(f"测试完成,订单号: {order_number}")

if name == "main ":

unittest.main()