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):

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

  public void testSignIn() {
    // fill login data on sign-in page
    driver.findElement(By.name("user_name")).sendKeys("testUser");
    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"));
  }
}

 

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 BaseSetup:

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

package com.anhtester.base;

import java.util.concurrent.TimeUnit;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Parameters;

public class BaseSetup {

	private WebDriver driver;
	
	static String driverPath = "resources\\drivers\\";

	public WebDriver getDriver() {
		return driver;
	}

	//Hàm này để tùy chọn Browser. Cho chạy trước khi gọi class này (BeforeClass)
	private void setDriver(String browserType, String appURL) {
		switch (browserType) {
		case "chrome":
			driver = initChromeDriver(appURL);
			break;
		case "firefox":
			driver = initFirefoxDriver(appURL);
			break;
		default:
			System.out.println("Browser: " + browserType + " is invalid, Launching Chrome as browser of choice...");
			driver = initChromeDriver(appURL);
		}
	}

	
	//Khởi tạo cấu hình của các Browser để đưa vào Switch Case
	
	private static WebDriver initChromeDriver(String appURL) {
		System.out.println("Launching Chrome browser...");
		System.setProperty("webdriver.chrome.driver", driverPath + "chromedriver.exe");
		WebDriver driver = new ChromeDriver();
		driver.manage().window().maximize();
		driver.navigate().to(appURL);
		driver.manage().timeouts().pageLoadTimeout(30, TimeUnit.SECONDS);
		driver.manage().timeouts().implicitlyWait(20, TimeUnit.SECONDS);
		return driver;
	}

	private static WebDriver initFirefoxDriver(String appURL) {
		System.out.println("Launching Firefox browser...");
		System.setProperty("webdriver.gecko.driver", driverPath + "geckodriver.exe");
		WebDriver driver = new FirefoxDriver();
		driver.manage().window().maximize();
		driver.navigate().to(appURL);
		driver.manage().timeouts().pageLoadTimeout(30, TimeUnit.SECONDS);
		driver.manage().timeouts().implicitlyWait(20, TimeUnit.SECONDS);
		return driver;
	}

	// Chạy hàm initializeTestBaseSetup trước hết khi class này được gọi
	@Parameters({ "browserType", "appURL" })
	@BeforeClass
	public void initializeTestBaseSetup(String browserType, String appURL) {
		try {
			// Khởi tạo driver và browser
			setDriver(browserType, appURL);
		} catch (Exception e) {
			System.out.println("Error..." + e.getStackTrace());
		}
	}

	@AfterClass
	public void tearDown() throws Exception {
		Thread.sleep(2000);
		driver.quit();
	}
}

 

Trang SignInPage:

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

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 SignInPage {

	private WebDriver driver;

	private By headerPageText = By.xpath("//a[normalize-space()='Forgot Username/Password?']");
	private By emailInput = By.id("EmailInputEmail");
	private By passwordInput = By.id("PasswordInputPassword");
	private By signinBtn = By.id("SignInButton");
	private By errorMsgText = By.id("signInError");

	private By pinInput = By.id("Pin");
	private By submitBtn = By.id("RequestPinForm_SubmitButton");
	private By backBtn = By.id("RequestPinForm_Back");
	private By resetPintBtn = By.id("RequestPinForm_ResetPin");

	// Khởi tạo class khi được gọi và truyền driver vào để các thành phần trong
	// class này đọc
	public SignInPage(WebDriver driver) {
		this.driver = driver;
	}

	public String getSignInPageTitle() {
		String pageTitle = driver.getTitle();
		return pageTitle;
	}

	public boolean verifySignInPageTitle() {
		String expectedTitle = "Sign In";
		return getSignInPageTitle().equals(expectedTitle);
	}

	public boolean verifySignInPageText() {
		WebElement element = driver.findElement(headerPageText);
		String pageText = element.getText();
		String expectedPageText = "Forgot Username/Password?";
		return pageText.contains(expectedPageText);
	}

	// Sau khi thực hiện click Submit thì khởi tạo trang DashboardPage
	public void signin(String username, String password, String Pin) throws Exception {
		enterEmail(username);
		enterPassword(password);
		clickSignIn();
		Thread.sleep(1000);
		enterPin(Pin);
		clickSubmit();
	}

	public boolean verifySignIn() {
		enterEmail("test");
		enterPassword("pass");
		clickSignIn();
		return getErrorMessage().contains("incorrect");
	}

	public void enterEmail(String email) {
		WebElement emailTxtBox = driver.findElement(emailInput);
		if (emailTxtBox.isDisplayed())
			emailTxtBox.sendKeys(email);
	}

	public void enterPassword(String password) {
		WebElement passwordTxtBox = driver.findElement(passwordInput);
		if (passwordTxtBox.isDisplayed())
			passwordTxtBox.sendKeys(password);
	}

	public void clickSignIn() {
		WebElement signin = driver.findElement(signinBtn);
		if (signin.isDisplayed()) {
			signin.click();
		}
	}

	public void clickSubmit() {
		WebElement submit = driver.findElement(submitBtn);
		if (submit.isDisplayed()) {
			submit.click();
		}
	}

	public void enterPin(String PIN) {
		driver.findElement(pinInput).sendKeys(PIN);
	}

	public void clickBack() {
		driver.findElement(backBtn).click();
	}

	public void clickResetPin() {
		driver.findElement(resetPintBtn).click();
	}

	public String getErrorMessage() {
		String strErrorMsg = null;
		WebElement errorMsg = driver.findElement(errorMsgText);
		if (errorMsg.isDisplayed() && errorMsg.isEnabled())
			strErrorMsg = errorMsg.getText();
		return strErrorMsg;
	}

	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á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 SignInPage(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 Sign In

 

Trang SignInTest:

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

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.base.BaseSetup;
import com.anhtester.pages.SignInPage;

public class SignInTest extends BaseSetup {

	private WebDriver driver;
	public SignInPage signInPage;

	@BeforeClass
	public void setUp() {
		driver = getDriver();
	}

	@Test()
	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");

	}

}

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

signInPage = new SignInPage(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.SignInTest">
				<methods>
					<include name="signIn" />
				</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.base.BaseSetup;
import com.anhtester.pages.DashboardPage;
import com.anhtester.pages.SignInPage;

public class DashboardTest extends BaseSetup {

	private WebDriver driver;
	public SignInPage signInPage;
	public DashboardPage dashboardPage;

	@BeforeClass
	public void setUp() {
		// Đã khởi tạo browser hết rồi kể cả wait, phóng to màn hình,...
		driver = getDriver();
	}
	
	//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.


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


🌱 Zalo
Automation Testing:   https://zalo.me/g/lsxswc560
🌱 Facebook Group: Cộng đồng Automation Testing Việt Nam (Website, Desktop, Mobile)
🌱 Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium

  • Anh Tester

    Đườ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:


🌱 Zalo
Automation Testing:   https://zalo.me/g/lsxswc560
🌱 Facebook Group: Cộng đồng Automation Testing Việt Nam (Website, Desktop, Mobile)
🌱 Facebook Fanpage: Cộng đồng Automation Testing Việt Nam - Selenium

Chia sẻ kiến thức lên trang

Bạn có thể đăng bài để chia sẻ kiến thức, bài viết của chính bạn lên trang Anh Tester Blog

Danh sách bài học