UI 自动化测试的 POM 设计模式实践
目录
UI 自动化测试的 POM 设计模式实践
一、背景
在 UI 自动化测试中,持续维护测试脚本的成本很高。页面稍微一改动,大量的测试用例就有可能失败,需要人工修改适配。
因此,有人提出了POM(Page Object Model)设计模式。核心思维是将页面结构和测试逻辑分离,使测试代码更易于维护和扩展。
二、什么是 POM
POM 的核心思想很简单:每个页面对应一个对象,页面上的元素和操作都封装在这个对象里。测试代码不直接操作 UI 元素,而是通过 Page Object 提供的方法来执行操作。
flowchart TD
subgraph Test[测试层]
A[测试用例 1]
B[测试用例 2]
C[测试用例 3]
end
subgraph Page[Page Object 层]
D[LoginPage]
E[HomePage]
F[DashboardPage]
end
subgraph Element[元素层]
G[定位器]
H[操作方法]
end
A & B & C --> D & E & F
D & E & F --> G & H
三、为什么需要 POM
1. 没有 POM 时的问题
def test_login():
driver.find_element(By.ID, "username").send_keys("test")
driver.find_element(By.ID, "password").send_keys("123456")
driver.find_element(By.CSS_SELECTOR, ".btn-primary").click()
assert "欢迎" in driver.page_source这种写法的问题:
- 元素定位散落各处:页面元素变化时需要修改大量代码来适配
- 业务语义不清晰:看懂代码需要耗费大量心智
- 代码重复严重:大量测试用例存在代码重复,没有复用
从而导致维护成本极高,开发稍微一改动,大量测试代码要跟着改。
2. 使用 POM 后
def test_login():
login_page = LoginPage(driver)
login_page.enter_username("test")
login_page.enter_password("123456")
login_page.click_login()
assert login_page.is_logged_in()- 元素定位收束:页面元素变化时只要修改一两次代码
- 业务语义清晰:一下看懂代码在做什么
- 模块化:减少重复代码
四、POM 设计实践
1. Page Object
class LoginPage:
"""登录页面对象"""
# 元素定位
USERNAME_INPUT = (By.ID, "username")
PASSWORD_INPUT = (By.ID, "password")
LOGIN_BUTTON = (By.CSS_SELECTOR, ".btn-primary")
ERROR_MESSAGE = (By.CLASS_NAME, "error-msg")
def __init__(self, driver):
self.driver = driver
self.driver.get("/login")
# 操作方法
def enter_username(self, username):
self.driver.find_element(*self.USERNAME_INPUT).send_keys(username)
return self # 链式调用
def enter_password(self, password):
self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password)
return self
def click_login(self):
self.driver.find_element(*self.LOGIN_BUTTON).click()
return HomePage(self.driver) # 将进入新的页面,于是此处返回下一个页面对象
def get_error_message(self):
return self.driver.find_element(*self.ERROR_MESSAGE).text
def is_logged_in(self):
return "/home" in self.driver.current_url2. 链式调用
def test_login_and_navigate():
dashboard = (LoginPage(driver)
.enter_username("admin")
.enter_password("123456")
.click_login() # 这里返回的是HomePage了
.navigate_to_dashboard()) # 这里是HomePage里的方法
assert dashboard.is_visible()3. 组件化设计
对于过于复杂的页面,还可以将页面拆分成多个组件:
class Header:
def __init__(self, driver):
self.driver = driver
def click_profile(self):
self.driver.find_element(By.ID, "header-profile").click()
return ProfilePage(self.driver)
def click_logout(self):
self.driver.find_element(By.ID, "header-logout").click()
return LoginPage(self.driver)
class Navigation:
def __init__(self, driver):
self.driver = driver
def goto_dashboard(self):
self.driver.find_element(By.LINK_TEXT, "Dashboard").click()
return DashboardPage(self.driver)
class DashboardPage:
def __init__(self, driver):
self.driver = driver
self.header = Header(driver)
self.navigation = Navigation(driver)4. 封装通用能力
class BasePage:
"""页面基类,封装通用能力"""
def __init__(self, driver, timeout=10):
self.driver = driver
self.timeout = timeout
def find_element(self, locator):
"""显式等待元素出现"""
return WebDriverWait(self.driver, self.timeout).until(
EC.presence_of_element_located(locator)
)
def find_elements(self, locator):
return WebDriverWait(self.driver, self.timeout).until(
EC.presence_of_all_elements_located(locator)
)
def click(self, locator):
"""可点击等待"""
element = self.find_element(locator)
element.click()
def is_element_visible(self, locator):
try:
return WebDriverWait(self.driver, 3).until(
EC.visibility_of_element_located(locator)
)
except TimeoutException:
return False五、进一步扩展
1. 封装元素
对于特别复杂的页面,可以直接在元素层面封装成独立对象:
class SearchBox:
def __init__(self, driver, locator):
self.driver = driver
self.locator = locator
@property
def input_field(self):
return self.driver.find_element(*self.locator, By.CSS_SELECTOR, "input")
def search(self, keyword):
self.input_field.clear()
self.input_field.send_keys(keyword)
self.input_field.send_keys(Keys.ENTER)
return SearchResultsPage(self.driver)
class HomePage:
def __init__(self, driver):
self.driver = driver
self.search_box = SearchBox(driver, (By.ID, "global-search"))2. 数据驱动
@dataclass
class LoginInfo:
username: str
password: str
expected_success: bool
class LoginPage:
def login(self, info: LoginInfo):
self.enter_username(info.username)
self.enter_password(info.password)
page = self.click_login()
if info.expected_success:
assert isinstance(page, HomePage)
else:
assert self.get_error_message() is not None
return page六、总结
好的 POM 设计能让测试代码像业务文档一样可读,同时极大降低维护成本。刚开始可能会觉得"多此一举",但当页面迭代、用例增长到一定规模时,POM 的价值会自然体现出来。