在Web UI自动化测试领域,PO(Page Object)模式是一种被广泛采用的设计模式。我第一次接触这个概念是在2015年参与一个电商平台测试项目时,当时我们的测试脚本经常因为页面元素变更而大面积失效,维护成本极高。PO模式的出现彻底改变了这种局面。
简单来说,PO模式的核心思想是将页面对象和测试逻辑分离。每个页面(或页面组件)对应一个类,这个类封装了该页面的所有元素定位和基础操作。测试用例则通过调用这些页面对象的方法来完成测试流程,不再直接操作页面元素。
重要提示:PO不是简单的元素定位集合,而是对页面功能和行为的抽象封装
在没有使用PO模式前,我们的测试脚本通常是这样的:
python复制def test_login():
driver.find_element(By.ID, "username").send_keys("admin")
driver.find_element(By.ID, "password").send_keys("123456")
driver.find_element(By.CLASS_NAME, "login-btn").click()
assert "Welcome" in driver.page_source
这种写法存在几个明显问题:
采用PO模式后,同样的测试用例可以这样写:
python复制def test_login():
login_page = LoginPage(driver)
home_page = login_page.login("admin", "123456")
assert home_page.is_welcome_message_displayed()
优势对比:
一个标准的Page类通常包含以下部分:
python复制class LoginPage:
def __init__(self, driver):
self.driver = driver
self.username = (By.ID, "username")
self.password = (By.ID, "password")
self.login_btn = (By.CLASS_NAME, "login-btn")
def enter_username(self, text):
self.driver.find_element(*self.username).send_keys(text)
def enter_password(self, text):
self.driver.find_element(*self.password).send_keys(text)
def click_login(self):
self.driver.find_element(*self.login_btn).click()
return HomePage(self.driver)
def login(self, username, password):
self.enter_username(username)
self.enter_password(password)
return self.click_login()
在实际项目中,我们可以对基础PO进行多项优化:
python复制from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
def wait_for_element(self, locator, timeout=10):
return WebDriverWait(self.driver, timeout).until(
EC.presence_of_element_located(locator)
)
python复制import logging
def click_login(self):
logging.info("Clicking login button")
element = self.wait_for_element(self.login_btn)
element.click()
return HomePage(self.driver)
python复制def take_screenshot(self, name):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"screenshots/{name}_{timestamp}.png"
self.driver.save_screenshot(filename)
return filename
推荐的项目目录结构:
code复制tests/
├── pages/
│ ├── base_page.py
│ ├── login_page.py
│ ├── home_page.py
│ └── ...
├── testcases/
│ ├── test_login.py
│ └── ...
└── conftest.py
创建基础Page类减少重复代码:
python复制class BasePage:
def __init__(self, driver):
self.driver = driver
self.timeout = 10
def find_element(self, locator):
return WebDriverWait(self.driver, self.timeout).until(
EC.presence_of_element_located(locator)
)
def click(self, locator):
self.find_element(locator).click()
def send_keys(self, locator, text):
self.find_element(locator).send_keys(text)
对于复杂页面,可以采用组件化方式:
python复制class HeaderComponent:
def __init__(self, driver):
self.driver = driver
self.profile_menu = (By.ID, "profile-menu")
def open_profile(self):
self.driver.find_element(*self.profile_menu).click()
return ProfilePage(self.driver)
class HomePage(BasePage):
def __init__(self, driver):
super().__init__(driver)
self.header = HeaderComponent(driver)
问题:当页面频繁改版时,元素定位需要大量更新
解决方案:
问题:如何准确判断页面加载完成
解决方案:
python复制def is_loaded(self):
try:
WebDriverWait(self.driver, self.timeout).until(
lambda d: d.execute_script("return document.readyState") == "complete"
)
return True
except TimeoutException:
return False
问题:动态加载内容导致元素找不到
解决方案:
python复制def wait_for_ajax(self):
WebDriverWait(self.driver, self.timeout).until(
lambda d: d.execute_script("return jQuery.active == 0")
)
可以与行为驱动开发(BDD)结合:
python复制@given('I am on login page')
def step_impl(context):
context.login_page = LoginPage(context.driver)
@when('I login with username "{username}" and password "{password}"')
def step_impl(context, username, password):
context.home_page = context.login_page.login(username, password)
Selenium提供的PageFactory模式:
java复制// Java示例
public class LoginPage {
@FindBy(id = "username")
WebElement username;
@FindBy(id = "password")
WebElement password;
public LoginPage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
}
新兴工具如Appium Studio、Katalon等提供了可视化元素定位和PO生成功能,可以大幅提升效率。
我在实际项目中发现,良好的PO设计可以使UI测试代码的维护成本降低60%以上。特别是在敏捷开发环境中,面对频繁的UI变更,PO模式的优势更加明显。