NỘI DUNG BÀI HỌC

Cấu trúc code auto test theo thiết kế Page Object Model (POM) trong Selenium Java

Sơ lược về Page Object - POM

  • OOP (Object-Oriented Programming) nhìn tất cả mọi vật là Object – đối tượng. Đối tượng được định nghĩa trong các class có các thuộc tính và các hành động khác nhau.
  • POM (Page Object Model) là 1 design pattern (mẫu thiết kế) giúp mô hình hóa các pages, hoặc các phần (component: header, footer, menu…) trong page của trang web thành mỗi đối tượng riêng biệt. Mỗi component / page sẽ gói gọn tất cả các hành động và các thuộc tính của component / page đó.

 

Ưu điểm của mô hình Page Object (POM - đối tượng trang)

  • Giúp bảo trì dễ dàng: POM hữu ích khi có sự thay đổi trong phần tử giao diện người dùng hoặc có sự thay đổi trong một hành động. Ví dụ: một menu thả xuống được thay đổi thành một nút radio. Trong trường hợp này, POM giúp xác định trang hoặc màn hình cần sửa đổi. Vì mọi màn hình sẽ có các tệp java khác nhau, nên việc xác định này là cần thiết để thực hiện các thay đổi bắt buộc đối với các tệp đó. Điều này làm cho các trường hợp kiểm thử dễ bảo trì và giảm lỗi.

  • Giúp sử dụng lại mã (dùng lại code đã viết): Như đã thảo luận, tất cả các màn hình đều độc lập. Bằng cách sử dụng POM, người ta có thể sử dụng mã thử nghiệm cho một màn hình và sử dụng lại nó trong một trường hợp thử nghiệm khác. Không cần phải viết lại mã, do đó tiết kiệm thời gian và công sức.

  • Dễ đọc code: Khi tất cả các màn hình có các tệp java độc lập, người ta có thể dễ dàng xác định các hành động sẽ được thực hiện trên một màn hình cụ thể bằng cách điều hướng qua tệp java đó thôi. Nếu một thay đổi ảnh hưởng đến một phần mã code nhất định thì nó có thể được thực hiện một cách hiệu quả mà không ảnh hưởng đến các tệp (class/package) khác.

  • Tạo kho lưu trữ: Có thể một kho lưu trữ duy nhất cho các xử lý chung hoặc hoạt động chung cho các trang thay vì có các xử lý này nằm rải rác trong các test case riêng lẻ. VD: getTitlePage(), verifyHeaderPage(),...


Phân tích code khi dùng mô hình POM

Trước tiên, hãy xem xét một ví dụ, điển hình của code auto không sử dụng đối tượng trang (POM):

Website Demo: https://crm.anhtester.com/admin/authentication

/***
 * Tests login feature
 */
public class LoginTest{

  @Test
  public void testLogin() {
    // fill login data on login page
    driver.get("https://crm.anhtester.com/admin/authentication");
    driver.findElement(By.id("email")).sendKeys("admin@example.com");
    driver.findElement(By.id("password")).sendKeys("123456");
    driver.findElement(By.xpath("//button[normalize-space()='Login']")).click();

    // verify menu Dashboard after login
    boolean checkMenu = driver.findElement(By.xpath("//span[normalize-space()='Dashboard']")).isDisplayed();
    Assert.assertTrue(checkMenu , "Login fail");
  }
}

 

Có hai vấn đề với cách tiếp cận này:

  • Không có sự tách biệt giữa phương pháp kiểm tra và bộ định vị trên UI (Locators trong ví dụ này). Cả các đối tượng element (id, xpath, name,...) và cách xử lý (click, sendKeys,..) cùng chung trong test case. Nếu giao diện người dùng của AUT thay đổi element, bố cục hoặc cách nhập và xử lý thông tin đăng nhập, thì tại các test case phải thay đổi đồng loạt. Rất mất thời gian và không hiệu quả. Nhiều khi nó sai sót sửa đi sửa lại hoài.

  • Locators sẽ được trải rộng trong nhiều testcase, mà tất cả các testcase khác phải sử dụng testcase đăng nhập này. Nó mà hỏng thì ngồi sửa hơi cực =))

Để phù hợp với Project, mình cũng cần tách biệt giữa các Page và Test. Ta không thể viết chung tất cả vào 1 class như chúng ta đã viết ở các bài trước. Ta cần tổ chức lại project 1 chút để code mình khi nhiều lên sẽ không bị rối ren.

[Selenium Java] Bài 17: Cấu trúc code theo Page Object Model (POM) | Anh Tester

Tạo cấu trúc source theo POM như sau:

[Selenium Java] Bài 17: Cấu trúc code theo Page Object Model (POM) | Anh Tester

Áp dụng các kỹ thuật POM, ví dụ trên có thể được viết lại như thế này trong ví dụ sau về đối tượng trang cho trang Đăng nhập.


Trang BaseTest:

Trang này chứa phần Khởi tạo Browser và đóng Driver

package com.anhtester.common;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.edge.EdgeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Optional;
import org.testng.annotations.Parameters;

import java.time.Duration;

public class BaseTest {

    public WebDriver driver;

    public void createBrowser() {
        driver = new ChromeDriver();
        driver.manage().window().maximize();
        driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(20));
        driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(40));
    }

    @BeforeMethod
    @Parameters({"browser"})
    public void createBrowser(@Optional("chrome") String browserName) {
        if (browserName.equals("chrome")) {
            driver = new ChromeDriver();
        }
        if (browserName.equals("edge")) {
            driver = new EdgeDriver();
        }
        if (browserName.equals("firefox")) {
            driver = new FirefoxDriver();
        }

        driver.manage().window().maximize();
        driver.manage().timeouts().pageLoadTimeout(Duration.ofSeconds(40));
    }

    @AfterMethod
    public void closeBrowser() {
        driver.quit();
    }

}

 

Trang LoginPage:

Trang này chứa phần Locators và xử lý trong trang Login

package com.anhtester.Bai17_PageObjectModel.pages;

import com.anhtester.constants.ConfigData;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;

import java.time.Duration;

public class LoginPage {
    //Khai báo driver cục bộ trong chính class này
    private WebDriver driver;
    private WebDriverWait wait;

    //Khai báo các element dạng đối tượng By (phương thức tìm kiếm)
    private By headerPage = By.xpath("//h1[normalize-space()='Login']");
    private By inputEmail = By.xpath("//input[@id='email']");
    private By inputPassword = By.xpath("//input[@id='password']");
    private By buttonLogin = By.xpath("//button[normalize-space()='Login']");
    private By errorMessage = By.xpath("//div[@id='alerts']");

    //Khai báo hàm xây dựng, để truyền driver từ bên ngoài vào chính class này sử dụng
    public LoginPage(WebDriver driver) {
        this.driver = driver;
        //driver = _driver;
        wait = new WebDriverWait(driver, Duration.ofSeconds(5));
    }

    private void setEmail(String email) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(inputEmail));
        driver.findElement(inputEmail).sendKeys(email);
    }

    private void setPassword(String password) {
        wait.until(ExpectedConditions.visibilityOfElementLocated(inputPassword));
        driver.findElement(inputPassword).sendKeys(password);
    }

    private void clickLoginButton() {
        wait.until(ExpectedConditions.visibilityOfElementLocated(buttonLogin));
        driver.findElement(buttonLogin).click();
    }

    public void verifyLoginSuccess() {
        Assert.assertFalse(driver.getCurrentUrl().contains("authentication"), "FAIL. Vẫn đang ở trang Login");
    }

    public void verifyLoginFail() {
        Assert.assertTrue(driver.getCurrentUrl().contains("authentication"), "FAIL. Không còn ở trang Login");
        Assert.assertTrue(driver.findElement(errorMessage).isDisplayed(), "Error message NOT displays");
        Assert.assertEquals(driver.findElement(errorMessage).getText(), "Invalid email or password", "Content of error massage NOT match.");
    }

    //Các hàm xử lý cho chính trang này
    public void loginCRM(String email, String password) {
        //https://crm.anhtester.com/admin/authentication
        driver.get(ConfigData.URL); //Gọi từ class ConfigData dạng biến static
        setEmail(email);
        setPassword(password);
        clickLoginButton();
    }
}

Cái class này hàm khởi tạo Object (constructor) có 1 tham số chính là driver. Nếu không có tham số này sẽ báo lỗi.

public LoginPage(WebDriver driver) {
    this.driver = driver;
}​

Trang này chứa các Element cùng Locators của chúng và các hàm xử lý trên trang Login

 

Trang LoginTest:

Trang này chạy test case xử lý các bước để Login và kế thừa lại class BaseTest để khởi tạo Browser

package com.anhtester.Bai17_PageObjectModel.testcases;

import com.anhtester.Bai17_PageObjectModel.pages.LoginPage;
import com.anhtester.common.BaseTest;
import org.testng.annotations.Test;

public class LoginTest extends BaseTest {

    LoginPage loginPage;

    @Test
    public void testLoginSuccess() {
        loginPage = new LoginPage(driver);

        //Gọi các hàm xử lý có sẵn để sử dụng
        loginPage.loginCRM("admin@example.com", "123456");
        loginPage.verifyLoginSuccess();
    }

    @Test
    public void testLoginWithEmailInvalid() {
        loginPage = new LoginPage(driver);

        //Gọi các hàm xử lý có sẵn để sử dụng
        loginPage.loginCRM("admin123@example.com", "123456");
        loginPage.verifyLoginFail();
    }
}

Từ class LoginTest, muốn sử dụng function ở class LoginPage, ta cần phải khởi tạo 1 Object của class đó.

loginPage = new LoginPage(driver);


Đoạn code trên các bạn thấy là tạo đối tượng của class SignInPage và gọi các hàm xử lý trong nó ra dùng thôi chứ không có viết Locators hay driver.findElement gì nữa cả.

Như vậy thì khi có vấn đề lỗi không tìm thấy Element hay sai cú pháp xử lý gì đó thì vào trang SignInPage sửa là xong.


Code cho file chạy XML:

<!DOCTYPE suite SYSTEM "http://testng.org/testng-1.0.dtd">
<suite name="Page Object test example">
	<parameter name="browserType" value="chrome" />
	<parameter name="appURL"
		value="https://anhtester.startkatalyst.com/account/sign-in" />

	<test name="sample test">
		<classes>
			<class name="com.anhtester.testcases.LoginTest">
				<methods>
					<include name="testLoginSuccess" />
				</methods>
			</class>
		</classes>
	</test>
</suite>


Trang DashboardPageDashboardTest y change vậy. Các bạn tự viết thử đi rồi có gì buổi sau An dô sửa.

Gợi ý trang DashboardPage:

package com.anhtester.pages;

import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.testng.Assert;

public class DashboardPage {
	
	private WebDriver driver;
	
	public String expectedTitle = "Dashboard";
	
	public String expectedPageText = "My Profile";
	
	private By DocumentModule = By.xpath("//p[normalize-space()='Document Management']");
	private By MyProfileModule = By.xpath("//p[normalize-space()='My Profile']");
	private By TrainingModule = By.xpath("//p[normalize-space()='Training Management']");
	
	private By headerPageText = By.xpath("//p[normalize-space()='My Profile']");
	
	public DashboardPage(WebDriver driver) {
		this.driver = driver;
	}
	
	public void openDocumentManagement()
	{
		driver.findElement(DocumentModule).click();
		//Đợi cho đến khi trang load xong với hàm bên dưới
		waitForPageLoaded();
	}

    //Hàm đợi trang load xong rồi thao tác
    public void waitForPageLoaded() {
        ExpectedCondition<Boolean> expectation = new
                ExpectedCondition<Boolean>() {
                    public Boolean apply(WebDriver driver) {
                        return ((JavascriptExecutor) driver).executeScript("return document.readyState").toString().equals("complete");
                    }
                };
        try {
            Thread.sleep(1000);
            WebDriverWait wait = new WebDriverWait(driver, 30);
            wait.until(expectation);
        } catch (Throwable error) {
            Assert.fail("Timeout waiting for Page Load Request to complete.");
        }
    }

}

Các bạn viết thêm các hàm xử lý trong trang Dashboard nhen.

Tiếp theo thì viết tiếp DashboardTest class tương tự SignInTest để gọi DashboardPage dùng các hàm đã viết xử lý sẵn dùng thôi.

package com.anhtester.testcases;

import org.openqa.selenium.WebDriver;
import org.testng.Assert;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;

import com.anhtester.common.BaseTest;
import com.anhtester.pages.DashboardPage;
import com.anhtester.pages.SignInPage;

public class DashboardTest extends BaseTest {

	public SignInPage signInPage;
	public DashboardPage dashboardPage;
	
	//SignIn mặc định
	@Test(priority = 1)
	public void signIn() throws Exception {
		System.out.println(driver);
		signInPage = new SignInPage(driver);

		Assert.assertTrue(signInPage.verifySignInPageTitle(), "Sign In page title doesn't match");
		Assert.assertTrue(signInPage.verifySignInPageText(), "Header page text not matching");
		
		signInPage.signin("thaian@mailinator.com", "Demo@123", "123456");

	}

	//Phần xử lý trang Dashboard
	@Test(priority = 2)
	public void openDocument() throws Exception {
		
		dashboardPage = new DashboardPage(driver);

		Assert.assertTrue(dashboardPage.verifySignInPageTitle(), "Dashboard page title doesn't match");
		dashboardPage.openDocumentManagement();

	}
}


Rồi viết thêm cái hàm cần chạy bỏ dô file XML bên trên hoặc thêm file XML mới.

==> Tạm thời kết thức phần POM cơ bản tại đây. Tiếp theo sẽ là Page Factory.

Teacher

Teacher

Anh Tester

Software Quality Engineer

Đường dẫu khó chân vẫn cần bước đi
Đời dẫu khổ tâm vẫn cần nghĩ thấu

Cộng đồng Automation Testing Việt Nam:

🌱 Telegram Automation Testing:   Cộng đồng Automation Testing
🌱 
Facebook Group Automation: Cộng đồng Automation Testing Việt Nam
🌱 
Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium
🌱 Telegram
Manual Testing:   Cộng đồng Manual Testing
🌱 
Facebook Group Manual: Cộng đồng Manual Testing Việt Nam

Chia sẻ khóa học lên trang

Bạn có thể đăng khóa học của chính bạn lên trang Anh Tester để kiếm tiền

Danh sách bài học