聊聊UI自動化的PageObject設計模式

Code~Rush發表於2022-04-10

 

當我們開發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模型?

 

隨著專案新需求的不斷迭代,開發程式碼和測試程式碼的複雜性增加。因此,開發自動化測試程式碼時必須遵循正確的專案結構。否則,程式碼可能會變得難以維護。

  1. Web由各種 WebElement(例如,選單項、文字框、核取方塊、單選按鈕等)的不同網頁組成。測試用例與這些元素互動,如果Selenium 定位器沒有以正確的方式管理,程式碼的複雜性將成倍增加。

  2. 測試程式碼的重複或定位器的重複使用會降低程式碼的可讀性,從而導致程式碼維護的開銷成本增加。例如,測試電子商務網站的登入功能,我們使用Selenium進行自動化測試,測試程式碼可以與網頁的底層 UI 或定位器進行互動。如果修改了UI或該頁面上元素的路徑發生了變化,會發生什麼情況?自動化測試用例將失敗,因為該用例執行的過程在網頁上找不到依賴的頁面元素。如果你對所有網頁採用相同的測試開發方法。在這種情況下,測試者必須花費大量精力來即時更新分散在不同頁面中的定位器。

 

PO模式優點

 

PageObject模型的優點

現在大家已經瞭解了PageObject設計模式的基礎知識,讓我們來看看使用該設計模式的一些優點:

  • 提高可重用性——不同 POM 類中的PageObject方法可以在不同的測試用例/測試套件中重用。因此,由於頁面方法的可重用性增加,整體程式碼量將大大減少。

  • 提升可維護性——由於測試場景和定位器是分開儲存的,它使程式碼更清晰,並且在維護測試程式碼上花費的精力更少。

  • 降低UI更改對用例造成的影響——即使 UI 中經常發生更改,也只需要在物件儲存庫(儲存定位器)中進行更改,對測試場景幾乎沒有影響。

  • 便與多個測試框架整合——由於測試實現與PageObject的儲存庫分離,我們可以將相同的儲存庫與不同的測試框架一起使用。例如,Test Case-1可以使用 Robot 框架,Tese Case - 2 可以使用 pytest 框架等,單個測試套件可以包含使用不同測試框架實現的測試用例。

 

PageObject實踐

 

首先我們先看一個反例,一個不使用PageObject模式的自動化測試示例(測試使用者登入場景):

 

/***
* 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模式,測試方法(登入)寫法如下:

 

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);
}
}

 

使用者登入以後的元素定位(用於斷言)方法寫法如下:

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,如下所示。

 

/***
* 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開發用例的注意事項:

  1. PageObject本身不進行斷言。斷言是測試用例的一部分,應該始終包含在測試程式碼中,即與測試內容相關的程式碼不應包含在PageObject中。

 

public void testMessagesAreReadOrUnread() {
Inbox inbox = new Inbox(driver);
inbox.assertMessageWithSubjectIsUnread("I like cheese");
inbox.assertMessageWithSubjectIsNotUnread("I'm not fond of tofu");
}

 

 

應該重寫為:

 

public void testMessagesAreReadOrUnread() {
Inbox inbox = new Inbox(driver);
assertTrue(inbox.isMessageWithSubjectIsUnread("I like cheese"));
assertFalse(inbox.isMessageWithSubjectIsUnread("I'm not fond of tofu"));
}

 

  1. 單一的驗證可以包含在PageObject內,即驗證頁面以及頁面上的關鍵元素是否正確載入,且此驗證應在例項化PageObject時完成。在上面的示例中, HomePage 建構函式檢查預期頁面是否載入完畢以執行測試程式碼。

 

附:以PageObject模式開發的完整的登入場景程式碼

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();
}
}

 

 

薦書

 

本書致力於幫助Python開發人員挖掘這門語言及相關程式庫的優秀特性,避免重複勞動,同時寫出簡潔、流暢、易讀、易維護,並且具有地道Python風格的程式碼。本書尤其深入探討了Python語言的高階用法,涵蓋資料結構、Python風格的物件、並行與併發,以及超程式設計等不同的方面。

 

加質量君微信,邀請進群免費領軟體測試進階測試開發1T自學視訊

 

相關文章