第六篇:《Page Object设计模式:让UI测试代码可维护、可复用》
随着UI测试用例增多,你会发现大量重复的定位器和操作代码散落在各处。一旦页面元素发生变化,需要修改几十个地方。Page Object模式是业界公认的解决方案。本文将用最直观的方式,带你从“脚本式”代码重构为“页面对象式”代码,并分享进阶技巧。
一、什么是不好的UI测试代码?
假设我们要测试登录功能,没有使用Page Object的代码可能长这样:
publicclassLoginTest{@TestpublicvoidtestLoginSuccess(){WebDriverdriver=newChromeDriver();driver.get("https://example.com/login");driver.findElement(By.id("username")).sendKeys("testuser");driver.findElement(By.id("password")).sendKeys("123456");driver.findElement(By.cssSelector("button[type='submit']")).click();Assert.assertTrue(driver.findElement(By.id("welcome")).isDisplayed());driver.quit();}@TestpublicvoidtestLoginWrongPassword(){WebDriverdriver=newChromeDriver();driver.get("https://example.com/login");driver.findElement(By.id("username")).sendKeys("testuser");driver.findElement(By.id("password")).sendKeys("wrong");driver.findElement(By.cssSelector("button[type='submit']")).click();Assert.assertTrue(driver.findElement(By.className("error-msg")).isDisplayed());driver.quit();}}问题:
定位器(如#username)在多个测试中重复
操作逻辑(输入用户名密码)重复
页面URL硬编码
如果登录页的id变了,需要修改所有测试方法
二、Page Object模式的核心思想
Page Object = 一个类对应一个页面,类中封装:
该页面的元素定位器(或WebElement对象)
该页面上的操作(方法)
返回其他Page Object的方法(用于流转)
原则:
测试用例只调用Page Object的方法,不直接操作WebDriver
Page Object的方法应返回有意义的东西(其他Page Object或断言数据)
不要在Page Object内部写断言,断言属于测试用例层
三、第一个Page Object:LoginPage
以登录页面为例。
Java版本
importorg.openqa.selenium.By;importorg.openqa.selenium.WebDriver;importorg.openqa.selenium.WebElement;importorg.openqa.selenium.support.FindBy;importorg.openqa.selenium.support.PageFactory;importorg.openqa.selenium.support.ui.ExpectedConditions;importorg.openqa.selenium.support.ui.WebDriverWait;publicclassLoginPage{privateWebDriverdriver;privateWebDriverWaitwait;// 方式1:使用By定位器privateByusernameInput=By.id("username");privateBypasswordInput=By.id("password");privateBysubmitButton=By.cssSelector("button[type='submit']");privateByerrorMsg=By.className("error-msg");// 方式2:使用@FindBy注解(需要PageFactory.initElements)@FindBy(id="username")privateWebElementusernameElem;publicLoginPage(WebDriverdriver){this.driver=driver;this.wait=newWebDriverWait(driver,Duration.ofSeconds(10));PageFactory.initElements(driver,this);// 初始化@FindBy}// 页面操作方法publicLoginPageenterUsername(Stringusername){wait.until(ExpectedConditions.visibilityOfElementLocated(usernameInput)).sendKeys(username);returnthis;// 返回自身支持链式调用}publicLoginPageenterPassword(Stringpassword){driver.findElement(passwordInput).sendKeys(password);returnthis;}publicHomePageclickSubmitSuccess(){driver.findElement(submitButton).click();returnnewHomePage(driver);// 成功后跳转到首页}publicLoginPageclickSubmitExpectingError(){driver.findElement(submitButton).click();returnthis;// 依然停留在登录页}publicStringgetErrorMessage(){returnwait.until(ExpectedConditions.visibilityOfElementLocated(errorMsg)).getText();}// 组合操作:完整登录流程publicHomePageloginAs(Stringusername,Stringpassword){returnenterUsername(username).enterPassword(password).clickSubmitSuccess();}}Python版本
fromselenium.webdriver.common.byimportByfromselenium.webdriver.remote.webdriverimportWebDriverfromselenium.webdriver.support.uiimportWebDriverWaitfromselenium.webdriver.supportimportexpected_conditionsasECclassLoginPage:# 定位器(类属性)USERNAME_INPUT=(By.ID,"username")PASSWORD_INPUT=(By.ID,"password")SUBMIT_BUTTON=(By.CSS_SELECTOR,"button[type='submit']")ERROR_MSG=(By.CLASS_NAME,"error-msg")def__init__(self,driver:WebDriver):self.driver=driver self.wait=WebDriverWait(driver,10)defenter_username(self,username:str):self.wait.until(EC.visibility_of_element_located(self.USERNAME_INPUT)).send_keys(username)returnselfdefenter_password(self,password:str):self.driver.find_element(*self.PASSWORD_INPUT).send_keys(password)returnselfdefclick_submit_success(self):self.driver.find_element(*self.SUBMIT_BUTTON).click()from.home_pageimportHomePage# 避免循环导入returnHomePage(self.driver)defclick_submit_expecting_error(self):self.driver.find_element(*self.SUBMIT_BUTTON).click()returnselfdefget_error_message(self)->str:returnself.wait.until(EC.visibility_of_element_located(self.ERROR_MSG)).textdeflogin_as(self,username:str,password:str):returnself.enter_username(username).enter_password(password).click_submit_success()四、HomePage示例
publicclassHomePage{privateWebDriverdriver;privateBywelcomeMsg=By.id("welcome");publicHomePage(WebDriverdriver){this.driver=driver;}publicbooleanisWelcomeDisplayed(){returndriver.findElement(welcomeMsg).isDisplayed();}publicStringgetWelcomeText(){returndriver.findElement(welcomeMsg).getText();}}五、使用Page Object的测试用例
Java (TestNG):
importorg.testng.Assert;importorg.testng.annotations.Test;publicclassLoginTest{privateWebDriverdriver;privateLoginPageloginPage;@BeforeMethodpublicvoidsetup(){driver=newChromeDriver();driver.get("https://example.com/login");loginPage=newLoginPage(driver);}@TestpublicvoidtestLoginSuccess(){HomePagehome=loginPage.loginAs("testuser","correct123");Assert.assertTrue(home.isWelcomeDisplayed(),"登录后未显示欢迎信息");}@TestpublicvoidtestLoginWrongPassword(){loginPage.loginAs("testuser","wrong");Stringerror=loginPage.getErrorMessage();Assert.assertTrue(error.contains("密码错误"));}@AfterMethodpublicvoidteardown(){if(driver!=null)driver.quit();}}Python (pytest):
importpytestfromseleniumimportwebdriverfrompages.login_pageimportLoginPageclassTestLogin:@pytest.fixturedefdriver(self):driver=webdriver.Chrome()driver.get("https://example.com/login")yielddriver driver.quit()deftest_login_success(self,driver):login_page=LoginPage(driver)home_page=login_page.login_as("testuser","correct123")asserthome_page.is_welcome_displayed()deftest_login_wrong_password(self,driver):login_page=LoginPage(driver)login_page.login_as("testuser","wrong")assert"密码错误"inlogin_page.get_error_message()六、进阶技巧
6.1 页面对象中的等待封装
不要在每个方法里都写WebDriverWait,可以在Page Object基类中封装。
publicabstractclassBasePage{protectedWebDriverdriver;protectedWebDriverWaitwait;publicBasePage(WebDriverdriver){this.driver=driver;this.wait=newWebDriverWait(driver,Duration.ofSeconds(10));}protectedvoidwaitForVisibility(Bylocator){wait.until(ExpectedConditions.visibilityOfElementLocated(locator));}protectedvoidclick(Bylocator){wait.until(ExpectedConditions.elementToBeClickable(locator)).click();}}6.2 页面对象之间的流转
方法返回新的Page Object,体现用户的操作流。
publicclassLoginPageextendsBasePage{publicHomePageloginSuccess(Stringuser,Stringpwd){// 输入并提交returnnewHomePage(driver);}}6.3 使用PageFactory(Java专用)
PageFactory.initElements可以延迟定位元素,每次访问时重新查找,避免StaleElementReferenceException。但需注意配合@CacheLookup使用。
publicclassLoginPage{@FindBy(id="username")privateWebElementusernameInput;publicLoginPage(WebDriverdriver){PageFactory.initElements(driver,this);}publicvoidenterUsername(Stringtext){usernameInput.sendKeys(text);// 每次调用时定位}}6.4 页面对象的单一职责
一个Page Object只对应一个页面或一个页面的大模块(如导航栏组件)。
不要将整个复杂页面的所有元素塞进一个类,可按组件拆分(如HeaderComponent、FooterComponent)。
七、Page Object的常见误区
八、总结与收益
采用Page Object模式后:
可维护性:页面修改只需改一个类,所有测试自动生效。
可复用性:多个测试可以共享同一个Page Object的方法。
可读性:测试代码读起来像业务剧本:loginPage.loginAs(“user”,“pwd”).clickSomething()。
并行开发:页面未完成时,可先定义接口,测试用例使用Mock。
作业:
