当我们开发UI自动化测试用例时,需要引用页面中的元素(数据)才能够进行点击(动作)并显示出页面内容。如果我们开发的用例是直接对HTML元素进行操作,则这样的用例无法“应对”页面中UI的更改。
PageObject模式就是对HTML页面以及元素细节的封装,并对外提供应用级别的API,使你摆脱与HTML的纠缠。
什么是PageObject模型?
PageObject模型是一种设计模式,其核心是减少代码重复(最小化代码更新/维护用例)以降低用例开发的工作量。利用PageObject模型,为每个网页创建Page类,测试场景中用的定位器/元素存储在单独的类文件中,并且测试用例在不同的文件中,使代码更加模块化。由于元素定位器和测试脚本是分开存储的,因此对 Web UI 元素的任何更改只需要在测试场景代码中进行更改即可。
基于PageObject模型的实现包含以下两点:
- Page类——将页面封装成 Page 类,页面元素为 Page 类的成员元素,页面功能放在 Page 类方法里。
- 测试类——针对这个 Page 类定义一个测试类,在测试类调用 Page 类的各个类方法完成测试。它使用Page类中的页面方法/方法与页面的 UI 元素进行交互。如果网页的UI有变化,只需要更新Page类,测试类无需改动。
为什么使用PageObject模型?
随着项目新需求的不断迭代,开发代码和测试代码的复杂性增加。因此,开发自动化测试代码时必须遵循正确的项目结构。否则,代码可能会变得难以维护。
- Web由各种 WebElement(例如,菜单项、文本框、复选框、单选按钮等)的不同网页组成。测试用例与这些元素交互,如果Selenium 定位器没有以正确的方式管理,代码的复杂性将成倍增加。
- 测试代码的重复或定位器的重复使用会降低代码的可读性,从而导致代码维护的开销成本增加。例如,测试电子商务网站的登录功能,我们使用Selenium进行自动化测试,测试代码可以与网页的底层 UI 或定位器进行交互。如果修改了UI或该页面上元素的路径发生了变化,会发生什么情况?自动化测试用例将失败,因为该用例执行的过程在网页上找不到依赖的页面元素。如果你对所有网页采用相同的测试开发方法。在这种情况下,测试者必须花费大量精力来即时更新分散在不同页面中的定位器。
PO模式优点
PageObject模型的优点
现在大家已经了解了PageObject设计模式的基础知识,让我们来看看使用该设计模式的一些优点:
- 提高可重用性——不同 POM 类中的PageObject方法可以在不同的测试用例/测试套件中重用。因此,由于页面方法的可重用性增加,整体代码量将大大减少。
- 提升可维护性——由于测试场景和定位器是分开存储的,它使代码更清晰,并且在维护测试代码上花费的精力更少。
- 降低UI更改对用例造成的影响——即使 UI 中经常发生更改,也只需要在对象存储库(存储定位器)中进行更改,对测试场景几乎没有影响。
- 便与多个测试框架集成——由于测试实现与PageObject的存储库分离,我们可以将相同的存储库与不同的测试框架一起使用。例如,Test Case-1可以使用 Robot 框架,Tese Case - 2 可以使用 pytest 框架等,单个测试套件可以包含使用不同测试框架实现的测试用例。
PageObject实践
首先我们先看一个反例,一个不使用PageObject模式的自动化测试示例(测试用户登录场景):
代码语言:javascript复制/***
* Tests login feature
*/
public class Login {
public void testLogin() {
// fill login data on sign-in page
driver.findElement(By.name("user_name")).sendKeys("userName");
driver.findElement(By.name("password")).sendKeys("my supersecret password");
driver.findElement(By.name("sign-in")).click();
// verify h1 tag is "Hello userName" after login
driver.findElement(By.tagName("h1")).isDisplayed();
assertThat(driver.findElement(By.tagName("h1")).getText(), is("Hello userName"));
}
}
这种写法有两个问题:
- 测试用例和 AUT 的定位器没有分离,两者耦合在一起。如果AUT的UI更改布局或登录的输入和处理方式,则用例本身必须更改。
- 如果多个页面都需要登录,则定位器将分布在多个测试用例中。
使用PageObject模式,测试方法(登录)写法如下:
代码语言:javascript复制
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
/**
* Page Object encapsulates the Sign-in page.
*/
public class SignInPage {
protected WebDriver driver;
// <input name="user_name" type="text" value="">
private By usernameBy = By.name("user_name");
// <input name="password" type="password" value="">
private By passwordBy = By.name("password");
// <input name="sign_in" type="submit" value="SignIn">
private By signinBy = By.name("sign_in");
public SignInPage(WebDriver driver){
this.driver = driver;
}
/**
* Login as valid user
*
* @param userName
* @param password
* @return HomePage object
*/
public HomePage loginValidUser(String userName, String password) {
driver.findElement(usernameBy).sendKeys(userName);
driver.findElement(passwordBy).sendKeys(password);
driver.findElement(signinBy).click();
return new HomePage(driver);
}
}
用户登录以后的元素定位(用于断言)方法写法如下:
代码语言:javascript复制import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
/**
* Page Object encapsulates the Home Page
*/
public class HomePage {
protected WebDriver driver;
// <h1>Hello userName</h1>
private By messageBy = By.tagName("h1");
public HomePage(WebDriver driver){
this.driver = driver;
if (!driver.getTitle().equals("Home Page of logged in user")) {
throw new IllegalStateException("This is not Home Page of logged in user,"
" current page is: " driver.getCurrentUrl());
}
}
/**
* Get message (h1 tag)
*
* @return String message text
*/
public String getMessageText() {
return driver.findElement(messageBy).getText();
}
public HomePage manageProfile() {
// Page encapsulation to manage profile functionality
return new HomePage(driver);
}
/* More methods offering the services represented by Home Page
of Logged User. These methods in turn might return more Page Objects
for example click on Compose mail button could return ComposeMail class object */
}
登录测试用例使用上述两个PageObject,如下所示。
代码语言:javascript复制/***
* Tests login feature
*/
public class TestLogin {
@Test
public void testLogin() {
SignInPage signInPage = new SignInPage(driver);
/// login
HomePage homePage = signInPage.loginValidUser("userName", "password");
// assert login result
assertThat(homePage.getMessageText(), is("Hello userName"));
}
}
注意事项
从上述例子中,可以看出PageObject的设计方式有很大的灵活性,这里也总结一下使用PageObject开发用例的注意事项:
- PageObject本身不进行断言。断言是测试用例的一部分,应该始终包含在测试代码中,即与测试内容相关的代码不应包含在PageObject中。
public void testMessagesAreReadOrUnread() {
Inbox inbox = new Inbox(driver);
inbox.assertMessageWithSubjectIsUnread("I like cheese");
inbox.assertMessageWithSubjectIsNotUnread("I'm not fond of tofu");
}
应该重写为:
代码语言:javascript复制public void testMessagesAreReadOrUnread() {
Inbox inbox = new Inbox(driver);
assertTrue(inbox.isMessageWithSubjectIsUnread("I like cheese"));
assertFalse(inbox.isMessageWithSubjectIsUnread("I'm not fond of tofu"));
}
- 单一的验证可以包含在PageObject内,即验证页面以及页面上的关键元素是否正确加载,且此验证应在实例化PageObject时完成。在上面的示例中, HomePage 构造函数检查预期页面是否加载完毕以执行测试代码。
附:以PageObject模式开发的完整的登录场景代码
代码语言:javascript复制public class LoginPage {
private final WebDriver driver;
public LoginPage(WebDriver driver) {
this.driver = driver;
// Check that we're on the right page.
if (!"Login".equals(driver.getTitle())) {
// Alternatively, we could navigate to the login page, perhaps logging out first
throw new IllegalStateException("This is not the login page");
}
}
// The login page contains several HTML elements that will be represented as WebElements.
// The locators for these elements should only be defined once.
By usernameLocator = By.id("username");
By passwordLocator = By.id("passwd");
By loginButtonLocator = By.id("login");
// The login page allows the user to type their username into the username field
public LoginPage typeUsername(String username) {
// This is the only place that "knows" how to enter a username
driver.findElement(usernameLocator).sendKeys(username);
// Return the current page object as this action doesn't navigate to a page represented by another PageObject
return this;
}
// The login page allows the user to type their password into the password field
public LoginPage typePassword(String password) {
// This is the only place that "knows" how to enter a password
driver.findElement(passwordLocator).sendKeys(password);
// Return the current page object as this action doesn't navigate to a page represented by another PageObject
return this;
}
// The login page allows the user to submit the login form
public HomePage submitLogin() {
// This is the only place that submits the login form and expects the destination to be the home page.
// A seperate method should be created for the instance of clicking login whilst expecting a login failure.
driver.findElement(loginButtonLocator).submit();
// Return a new page object representing the destination. Should the login page ever
// go somewhere else (for example, a legal disclaimer) then changing the method signature
// for this method will mean that all tests that rely on this behaviour won't compile.
return new HomePage(driver);
}
// The login page allows the user to submit the login form knowing that an invalid username and / or password were entered
public LoginPage submitLoginExpectingFailure() {
// This is the only place that submits the login form and expects the destination to be the login page due to login failure.
driver.findElement(loginButtonLocator).submit();
// Return a new page object representing the destination. Should the user ever be navigated to the home page after submiting a login with credentials
// expected to fail login, the script will fail when it attempts to instantiate the LoginPage PageObject.
return new LoginPage(driver);
}
// Conceptually, the login page offers the user the service of being able to "log into"
// the application using a user name and password.
public HomePage loginAs(String username, String password) {
// The PageObject methods that enter username, password & submit login have already defined and should not be repeated here.
typeUsername(username);
typePassword(password);
return submitLogin();
}
}