跳转至

PO模式完整实现与最佳实践

本节通过完整的测试代码示例展示PO模式的实际应用,并总结PO模式的最佳实践指南。

完整实现示例

测试代码示例

Python
import unittest
from selenium import webdriver

class TestLogin(unittest.TestCase):
    """登录功能测试"""

    def setUp(self):
        """测试前准备"""
        self.driver = webdriver.Chrome()
        self.login_proxy = LoginProxy(self.driver)

    def tearDown(self):
        """测试后清理"""
        self.driver.quit()

    def test_valid_login(self):
        """测试有效登录"""
        success, message = self.login_proxy.login_with_valid_credentials()
        self.assertTrue(success, message)
        print(f"✅ {message}")

    def test_invalid_login(self):
        """测试无效登录"""
        success, message = self.login_proxy.login_with_invalid_credentials()
        self.assertTrue(success, message)
        print(f"✅ {message}")

    def test_login_with_remember_me(self):
        """测试记住我功能"""
        success, message = self.login_proxy.login_with_remember_me()
        self.assertTrue(success, message)
        print(f"✅ {message}")

    def test_admin_quick_login(self):
        """测试管理员快速登录"""
        success, message = self.login_proxy.quick_login("admin")
        self.assertTrue(success, message)
        print(f"✅ {message}")

if __name__ == '__main__':
    unittest.main(verbosity=2)

对比分析

使用PO模式前

Python
def test_login():
    driver = webdriver.Chrome()
    driver.get("https://example.com/login")
    driver.find_element(By.ID, "username").send_keys("testuser")
    driver.find_element(By.ID, "password").send_keys("testpass")
    driver.find_element(By.ID, "login-btn").click()
    assert "dashboard" in driver.current_url
    driver.quit()

使用PO模式后

Python
def test_login():
    driver = webdriver.Chrome()
    login_proxy = LoginProxy(driver)
    success, message = login_proxy.login_with_valid_credentials()
    assert success, message
    driver.quit()

复杂业务场景示例

电商网站完整购物流程

Python
class ECommerceProxy:
    """电商网站业务代理类"""

    def __init__(self, driver):
        self.driver = driver
        self.login_proxy = LoginProxy(driver)
        self.product_handle = ProductHandle(driver)
        self.cart_handle = CartHandle(driver)
        self.checkout_handle = CheckoutHandle(driver)

    def complete_purchase_flow(self, username, password, product_name, quantity=1):
        """完整的购买流程"""
        try:
            # 1. 用户登录
            success, message = self.login_proxy.login_with_valid_credentials(username, password)
            if not success:
                return False, f"登录失败: {message}"

            # 2. 搜索产品
            self.product_handle.search_product(product_name)
            product_found = self.product_handle.is_product_available(product_name)
            if not product_found:
                return False, f"产品 {product_name} 未找到"

            # 3. 添加到购物车
            self.product_handle.add_to_cart(product_name, quantity)

            # 4. 检查购物车
            cart_items = self.cart_handle.get_cart_items()
            if product_name not in [item['name'] for item in cart_items]:
                return False, "产品未成功添加到购物车"

            # 5. 结账
            self.checkout_handle.proceed_to_checkout()
            order_number = self.checkout_handle.complete_checkout()

            return True, f"购买成功,订单号: {order_number}"

        except Exception as e:
            return False, f"购买流程失败: {str(e)}"

    def batch_add_products(self, username, password, product_list):
        """批量添加产品到购物车"""
        try:
            # 登录
            success, message = self.login_proxy.login_with_valid_credentials(username, password)
            if not success:
                return False, f"登录失败: {message}"

            added_products = []
            failed_products = []

            for product_name, quantity in product_list:
                try:
                    self.product_handle.search_product(product_name)
                    if self.product_handle.is_product_available(product_name):
                        self.product_handle.add_to_cart(product_name, quantity)
                        added_products.append(product_name)
                    else:
                        failed_products.append(f"{product_name}(未找到)")
                except Exception as e:
                    failed_products.append(f"{product_name}(添加失败: {str(e)})")

            result_message = f"成功添加: {len(added_products)}个产品"
            if failed_products:
                result_message += f", 失败: {len(failed_products)}个产品"

            return True, result_message

        except Exception as e:
            return False, f"批量添加失败: {str(e)}"

PO模式最佳实践

1. 命名规范

Python
# 页面对象类:以Page结尾
class LoginPage:
    """登录页面对象"""
    pass

class ProductSearchPage:
    """产品搜索页面对象"""
    pass

# 操作类:以Handle结尾
class LoginHandle:
    """登录操作处理类"""
    pass

class ShoppingCartHandle:
    """购物车操作处理类"""
    pass

# 业务代理类:以Proxy结尾
class UserManagementProxy:
    """用户管理业务代理类"""
    pass

class OrderManagementProxy:
    """订单管理业务代理类"""
    pass

# 定位器:全大写,描述性命名
USERNAME_INPUT = (By.ID, "username")
SUBMIT_BUTTON = (By.CSS_SELECTOR, ".submit-btn")
ERROR_MESSAGE_CONTAINER = (By.CLASS_NAME, "error-container")

2. 方法设计原则

Python
class PageHandle:
    """页面操作基类"""

    def operation_method(self):
        """操作方法应该返回self,支持链式调用"""
        # 执行操作
        return self

    def get_something(self):
        """获取类方法,返回具体值"""
        element = self.page.get_element()
        return element.text

    def is_something_displayed(self):
        """判断类方法,返回布尔值"""
        try:
            self.page.get_element()
            return True
        except:
            return False

    def wait_for_condition(self, timeout=10):
        """等待类方法,返回self或抛出异常"""
        # 等待逻辑
        return self

# 链式调用示例
login_handle = LoginHandle(driver)
result = (login_handle
          .navigate_to_login_page()
          .input_username("testuser")
          .input_password("testpass")
          .click_login_button()
          .wait_for_login_success())

3. 错误处理策略

Python
class LoginProxy:
    """登录业务代理类 - 错误处理示例"""

    def safe_login(self, username, password):
        """安全登录方法 - 完整错误处理"""
        try:
            self.login_handle.navigate_to_login_page()
            self.login_handle.input_username(username)
            self.login_handle.input_password(password)
            self.login_handle.click_login_button()
            self.login_handle.wait_for_login_success()
            return True, "登录成功"

        except TimeoutException:
            return False, "页面加载超时,请检查网络连接"
        except NoSuchElementException as e:
            return False, f"页面元素未找到: {str(e)}"
        except WebDriverException as e:
            return False, f"浏览器操作失败: {str(e)}"
        except Exception as e:
            return False, f"未知错误: {str(e)}"

    def retry_login(self, username, password, max_attempts=3):
        """重试登录机制"""
        for attempt in range(max_attempts):
            success, message = self.safe_login(username, password)
            if success:
                return True, f"登录成功(第{attempt + 1}次尝试)"

            if attempt < max_attempts - 1:
                time.sleep(2)  # 等待2秒后重试

        return False, f"登录失败,已尝试{max_attempts}次"

4. 配置管理

Python
class TestConfig:
    """测试配置类"""

    # 环境配置
    BASE_URL = "https://example.com"
    TIMEOUT = 10

    # 用户凭据
    VALID_USER = {
        "username": "testuser",
        "password": "testpass"
    }

    ADMIN_USER = {
        "username": "admin",
        "password": "admin123"
    }

    # 页面URL
    LOGIN_URL = f"{BASE_URL}/login"
    DASHBOARD_URL = f"{BASE_URL}/dashboard"
    PRODUCTS_URL = f"{BASE_URL}/products"

class LoginProxy:
    """使用配置的登录代理类"""

    def __init__(self, driver):
        self.driver = driver
        self.config = TestConfig()
        self.login_handle = LoginHandle(driver)

    def quick_valid_login(self):
        """快速有效登录"""
        user = self.config.VALID_USER
        return self.login_with_credentials(user["username"], user["password"])

    def quick_admin_login(self):
        """快速管理员登录"""
        admin = self.config.ADMIN_USER
        return self.login_with_credentials(admin["username"], admin["password"])

5. 等待策略优化

Python
class BasePage:
    """基础页面类 - 等待策略"""

    def __init__(self, driver, timeout=10):
        self.driver = driver
        self.wait = WebDriverWait(driver, timeout)

    def wait_for_element_visible(self, locator):
        """等待元素可见"""
        return self.wait.until(EC.visibility_of_element_located(locator))

    def wait_for_element_clickable(self, locator):
        """等待元素可点击"""
        return self.wait.until(EC.element_to_be_clickable(locator))

    def wait_for_text_present(self, locator, text):
        """等待文本出现"""
        return self.wait.until(EC.text_to_be_present_in_element(locator, text))

    def wait_for_url_contains(self, url_part):
        """等待URL包含特定字符串"""
        return self.wait.until(EC.url_contains(url_part))

class LoginPage(BasePage):
    """继承基础页面的登录页面"""

    USERNAME_INPUT = (By.ID, "username")
    PASSWORD_INPUT = (By.ID, "password")
    LOGIN_BUTTON = (By.ID, "login-btn")

    def get_username_input(self):
        return self.wait_for_element_visible(self.USERNAME_INPUT)

    def get_password_input(self):
        return self.wait_for_element_visible(self.PASSWORD_INPUT)

    def get_login_button(self):
        return self.wait_for_element_clickable(self.LOGIN_BUTTON)

6. 数据驱动测试集成

Python
class DataDrivenLoginProxy:
    """数据驱动的登录代理类"""

    def __init__(self, driver):
        self.driver = driver
        self.login_handle = LoginHandle(driver)

    def test_login_with_data_set(self, test_data_list):
        """使用数据集测试登录"""
        results = []

        for test_data in test_data_list:
            username = test_data.get("username")
            password = test_data.get("password")
            expected_result = test_data.get("expected")
            test_description = test_data.get("description", "")

            success, message = self.login_handle.attempt_login(username, password)

            test_result = {
                "description": test_description,
                "username": username,
                "expected": expected_result,
                "actual": success,
                "message": message,
                "passed": success == expected_result
            }

            results.append(test_result)

        return results

# 使用示例
test_data = [
    {
        "username": "validuser",
        "password": "validpass",
        "expected": True,
        "description": "有效凭据登录"
    },
    {
        "username": "invaliduser",
        "password": "invalidpass",
        "expected": False,
        "description": "无效凭据登录"
    },
    {
        "username": "",
        "password": "validpass",
        "expected": False,
        "description": "空用户名登录"
    }
]

login_proxy = DataDrivenLoginProxy(driver)
results = login_proxy.test_login_with_data_set(test_data)

总结

PO模式的关键要点

  1. 三层架构:Page层负责元素定位,Handle层负责操作封装,Proxy层负责业务逻辑
  2. 单向依赖:上层依赖下层,避免循环依赖
  3. 职责分离:每层专注于自己的职责,保持接口稳定
  4. 链式调用:支持方法链式调用,提高代码流畅性
  5. 错误处理:完善的异常处理机制,提高测试稳定性

适用场景

  • 大型测试项目:页面数量多,测试用例复杂
  • 团队开发:多人协作,需要统一的代码规范
  • 长期维护:项目需要长期维护和扩展
  • UI变化频繁:页面结构经常变化,需要降低维护成本

实施建议

  1. 循序渐进:从简单页面开始,逐步应用PO模式
  2. 团队培训:确保团队成员理解PO模式的设计思想
  3. 代码评审:通过代码评审确保PO模式实施质量
  4. 持续重构:根据项目发展持续优化PO模式实现