Page Object Model ( POM ) là một cấu trúc code vô cùng quen thuộc trong automation test, Page Factory là phần mở rộng của POM, hôm nay mình sẽ trình bày cách sử dụng POM và Page Factory trong automation test để giúp chúng ta maintain code dễ dàng hơn
Page Object Model
Về định nghĩa: có thể hiểu nôm na rằng POM là một cấu trúc code mà ta xem mỗi một page của một trang web là một class (hompage, register page, payment page,… ) trong class đó ta sẽ define tất cả các element ( các thuộc tính của trang web như textbox, button, … ) và action liên quan đến các element đó.
Để mọi người có thể hình dung rõ ràng nhất về việc tại sao chúng ta nên apply POM vào trong automation test, mọi người có thể xem đoạn code bên dưới khi không apply POM sẽ như thế này:
WebDriver driver;
String url = "https://www.demoblaze.com/";
@BeforeClass
public void beforeClass(){
driver = new FirefoxDriver();
driver.get(url);
}
// Đăng ký với empty data
@Test
public void TC001_EmptyData(){
driver.findElement(By.xpath("//a[text()='Sign up']")).click();
driver.findElement(By.xpath("//button[text()='Sign up']")).click();
Alert waitElement = new WebDriverWait(driver, Duration.ofSeconds(30)).until(ExpectedConditions.alertIsPresent());
waitElement.accept();
}
// Đăng ký với invalid email
@Test
public void TC002_RegisterSuccessl(){
driver.findElement(By.xpath("//a[text()='Sign up']")).click();
driver.findElement(By.xpath("//input[@id='sign-username']")).sendKeys("testabc");
driver.findElement(By.xpath("//input[@id='sign-password']")).sendKeys("123456");
driver.findElement(By.xpath("//button[text()='Sign up']")).click();
}
@AfterClass
public void afterClass(){
if (driver != null) {
driver.quit();
}
}
Còn đây là đoạn code khi ta apply POM:
WebDriver driver;
HomePageObject homePageObject;
SignUpPageObject registerPageObject;
String url = "https://www.demoblaze.com/";
@BeforeClass
public void beforeClass(){
driver = getBrowserDrive("FIREFOX");
homePageObject = new HomePageObject(driver);
registerPageObject = new SignUpPageObject(driver);
driver.get(url);
}
// Đăng ký với empty data
@Test
public void TC001_EmptyData(){
homePageObject.clickToRegisterLink();
registerPageObject.clickToRegisterBtn();
registerPageObject.acceptAlert();
}
// Đăng ký với invalid email
@Test
public void TC002_RegisterSuccessl(){
homePageObject.clickToRegisterLink();
registerPageObject.inputToUsername("testabc");
registerPageObject.inputToPassword("123456");
registerPageObject.clickToRegisterBtn();
}
@AfterClass
public void afterClass(){
if (driver != null) {
driver.quit();
}
}
Rõ ràng ở đoạn code apply POM mọi người có thể thấy:
- Code dễ đọc dễ hiểu hơn đúng không ạ, các bạn có thể biết ngay là step này là click vào register này, step này là input username này…
- Việc update thay đổi chắc chắn sẽ dễ dàng hơn, chứ không phải sửa trực tiếp trong class Testcase ( sửa ở đâu ta chưa biết hihi ).
Từ đó ta có thể thấy các lợi ích của POM là:
Dễ maintainance code:
Giả sử khi trang web của bạn có sự thay đổi, ví dụ như btn “Login” có sự thay đổi locator, hay dropdown list bị thay thế bằng radio button, bạn sẽ không phải đi mò trong hàng chục class để xem chỗ nào thay đổi và chỉnh sửa, mà chỉ cần đến đúng page class đó và sửa đổi, điều này giúp việc maintain trở nên dễ dàng và giảm thiêu sai sót hơn.
Tái sử dụng code:
Như đã đề cập, tất cả page của trang web/app đều được coi là một class riêng rẽ => bạn có thể sử dụng code của một màn hình trong nhiều testcase giúp chúng ta tiết kiệm rất nhiều thời gian và công sức. Với POM, bạn có thể sử dụng mã kiểm thử của một màn hình và tái sử dụng nó trong các test case khác mà không cần viết lại mã, tiết kiệm thời gian và công sức.
Code dễ đọc và đáng tin cậy hơn:
Như 2 ví dụ ở trên ta thấy rõ code theo cấu trúc POM dễ đọc dễ hiểu hơn rất nhiều, đồng thời với sự độc lập của các class ( page ), ta có thể dễ dàng define được các action cần thực hiện trên màn hình đó và khi có sự thay đổi ta dễ dàng thực hiện mà không ảnh hưởng đến các class khác.
Bây giờ ta sẽ thực hiện một testcase có apply POM nhé:
Testcase của mình sẽ là:
TC1:
Step1: User vào homepage của trang : https://www.browserstack.com/ -> Step này verify đã hiển thị header của page
Step2: Click “get started”
TC2:
Precondition: done TC1
Step1: verify hiển thị header
Step2: fill các field full name, email, password
Trước hết chúng ta tạo 2 Page Object class: BrowserStackHomePage Java File và BrowserStackSignUpPage Java File
BrowserStackHomePage
package browserStackPages;
import static org.testng.Assert.assertEquals;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class BrowserStackHomePage {
WebDriver driver;
By Header=By.xpath("//h1");
By getStarted=By.xpath("//*[@id='signupModalProductButton']");
public BrowserStackHomePage(WebDriver driver) {
this.driver=driver;
}
public void veryHeader() {
String getheadertext=driver.findElement(Header).getText();
assertEquals("App & Browser Testing Made Easy", getheadertext);
}
public void clickOnGetStarted() {
driver.findElement(getStarted).click();
}
}
BrowserStackSignUpPage
package browserStackPages;
import static org.testng.Assert.assertEquals;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
public class BrowserStackSignUpPage {
WebDriver driver;
By Header = By.xpath("//h1[contains(text(), 'Create')]");
By userName = By.xpath("//*[@id='user_full_name']");
By businessEmail = By.xpath("//*[@id='user_email_login']");
By password = By.xpath("//*[@id='user_password']");
public BrowserStackSignUpPage(WebDriver driver) {
this.driver = driver;
}
public void veryHeader() {
String getheadertext = driver.findElement(Header).getText().trim();
assertEquals("Create a FREE Account", getheadertext);
}
public void enterFullName(String arg1) {
driver.findElement(userName).sendKeys(arg1);
}
public void enterBusinessEmail(String arg1) {
driver.findElement(businessEmail).sendKeys(arg1);
}
public void enterPasswrod(String arg1) {
driver.findElement(password).sendKeys(arg1);
}
}
Sau khi đã tạo 2 class PageObject rồi
Giờ ta sẽ implement vào class testcase
package browserStackSetup;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import browserStackPages.BrowserStackHomePage;
import browserStackPages.BrowserStackSignUpPage;
public class BrowserStackSetup {
String driverPath = "C:\\geckodriver.exe";
WebDriver driver;
BrowserStackHomePage objBrowserStackHomePage;
BrowserStackSignUpPage objBrowserStackSignUpPage;
@BeforeTest
public void setup() {
System.setProperty("webdriver.chrome.driver", "C:\\BrowserStack\\chromedriver.exe");
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.get("https://www.browserstack.com/");
}
@Test(priority = 1)
public void navigate_to_homepage_click_on_getstarted() {
objBrowserStackHomePage = new BrowserStackHomePage(driver);
objBrowserStackHomePage.veryHeader();
objBrowserStackHomePage.clickOnGetStarted();
}
@Test(priority = 2)
public void enter_userDetails() {
objBrowserStackSignUpPage = new BrowserStackSignUpPage(driver);
objBrowserStackSignUpPage.veryHeader();
objBrowserStackSignUpPage.enterFullName("TestUser");
objBrowserStackSignUpPage.enterBusinessEmail("TestUser@gmail.com");
objBrowserStackSignUpPage.enterPasswrod("TestUserPassword");
}
@AfterTest
public void afterTest() {
driver.quit();
}
}
Từ các đoạn trên bạn đã thấy rõ việc apply POM vào một testcase sẽ như thế nào.
Page Factory
Page Factory là một class được cung cấp bởi Selenium Webdriver để support cho POM. Trong đó tester có thể sử dụng anotaion @FindBy để khai báo 1 WebElement
Dưới đây là một đoạn code apply cả POM và Page Factory
BrowserStackHomePage:
package browserStackPages;
import static org.testng.Assert.assertEquals;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class BrowserStackHomePage {
WebDriver driver;
@FindBy(xpath = "//h1")
WebElement Header;
@FindBy(xpath = "//*[@id='signupModalProductButton']")
WebElement getStarted;
public BrowserStackHomePage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void veryHeader() {
String getheadertext = Header.getText();
assertEquals("App & Browser Testing Made Easy", getheadertext);
}
public void clickOnGetStarted() {
getStarted.click();
}
}
BrowserStackSignUp Page:
package browserStackPages;
import static org.testng.Assert.assertEquals;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.FindBy;
import org.openqa.selenium.support.PageFactory;
public class BrowserStackSignUpPage {
WebDriver driver;
@FindBy(xpath = "//h1[contains(text(), 'Create')]")
WebElement Header;
@FindBy(xpath = "//*[@id='user_full_name']")
WebElement userName;
@FindBy(xpath = "//*[@id='user_email_login']")
WebElement businessEmail;
@FindBy(xpath = "//*[@id='user_password']")
WebElement password;
public BrowserStackSignUpPage(WebDriver driver) {
this.driver = driver;
PageFactory.initElements(driver, this);
}
public void veryHeader() {
String getheadertext = Header.getText().trim();
assertEquals("Create a FREE Account", getheadertext);
}
public void enterFullName(String arg1) {
userName.sendKeys(arg1);
}
public void enterBusinessEmail(String arg1) {
businessEmail.sendKeys(arg1);
}
public void enterPasswrod(String arg1) {
password.sendKeys(arg1);
}
}
Với class testcase thì vẫn không có gì thay đổi
package browserStackSetup;
import java.util.concurrent.TimeUnit;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.testng.annotations.BeforeTest;
import org.testng.annotations.Test;
import browserStackPages.BrowserStackHomePage;
import browserStackPages.BrowserStackSignUpPage;
public class BrowserStackSetup {
String driverPath = "C:\\geckodriver.exe";
WebDriver driver;
BrowserStackHomePage objBrowserStackHomePage;
BrowserStackSignUpPage objBrowserStackSignUpPage;
@BeforeTest
public void setup() {
System.setProperty("webdriver.chrome.driver", "C:\\BrowserStack\\chromedriver.exe");
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS);
driver.get("https://www.browserstack.com/");
}
@Test(priority = 1)
public void navigate_to_homepage_click_on_getstarted() {
objBrowserStackHomePage = new BrowserStackHomePage(driver);
objBrowserStackHomePage.veryHeader();
objBrowserStackHomePage.clickOnGetStarted();
}
@Test(priority = 2)
public void enter_userDetails() {
objBrowserStackSignUpPage = new BrowserStackSignUpPage(driver);
objBrowserStackSignUpPage.veryHeader();
objBrowserStackSignUpPage.enterFullName("TestUser");
objBrowserStackSignUpPage.enterBusinessEmail("TestUser@gmail.com");
objBrowserStackSignUpPage.enterPasswrod("TestUserPassword");
}
@AfterTest
public void afterTest() {
driver.quit();
}
}
Ta thấy rõ một cách khai báo element mới đó là dùng anotation @FindBy, như code trên là find với xpath nhưng bạn cũng có thể thay bằng các locator khác như: ClassName, CSS, Name, TagName, LinkText, PartialLinkText.
Việc khởi tạo class cũng được bổ sung thêm việt init PageFactory: PageFactory.initElements(driver, this);
Vậy chúng ta sẽ đặt câu hỏi thế tại sao lại cần sử dụng PageFactory, tôi dùng các cách findElement khác cũng được mà ? Câu trả lời : có hoặc không ở đây còn tùy vào tình huống.
Cơ chế hoạt động của Page Factory là gì :
Khi ta khai báo một element với @FindBy, nó sẽ tạo ra một object để đại diện cho element đó.
Trong Page Factory có một tính chất là Lazy Initalization ( khới tạo chậm ), khi element có sự tương tác như click() hoặc getText(), sendKey…. thì nó mới được tìm => tối ưu về hiệu suất.
Nhưng vì ta đã khai báo một cách cố định từ trước dẫn đến khi DOM hoặc element đó thay đổi locator sau khi reload chẳng hạn => xảy ra lỗi StaleElementReferenceException hoặc NoSuchElementException.
Vậy tóm tại khi nào nên sử dụng Page Factory khi nào không ? Theo mình thấy thế này
Nên:
- Dự án nhỏ với nhiều ít page, scope kiểm thử bé.
- locator của element ít có sự thay đổi (ít AJAX hoặc reload DOM).
- Tăng tốc độ code, giảm lặp code
Không nên:
Web của bạn động ( phụ thuộc nhiều vào AJAX hoặc WebSocket ), tester cần control việc tìm và tương tác với các WebElement hoặc đơn giản là e ngại việc gặp các lỗi
Trang web nhiều nội dung động, phụ thuộc nhiều vào AJAX hoặc WebSocket.Bạn cần kiểm soát linh hoạt việc tìm kiếm và tương tác với các WebElements.Bạn lo ngại về StaleElementReferenceException hoặc NoSuchElementException. và muốn tìm giải pháp linh hoạt hơn (dynamic locating).
Bài viết đến đây là khá dài, hi vọng mình trình bày đủ clear để mọi người hiểu được hihi
Nội dung blog có tham khảo từ bài viết này: https://www.browserstack.com/guide/page-object-model-in-selenium