UI 自动化的 PageObject 设计模式

目录

前言:

[什么是 PageObject 模型?](#什么是 PageObject 模型?)

[为什么使用 PageObject 模型?](#为什么使用 PageObject 模型?)

[PO 模式优点](#PO 模式优点)

[PageObject 实践](#PageObject 实践)


前言:

UI 自动化是一种软件测试方法,它主要用于检查应用程序的用户界面是否符合预期。PageObject 是 UI 自动化中的一个重要概念,它是一种将页面元素映射到 Python 对象的设计模式。

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 模式的自动化测试示例(测试用户登录场景):

/***
* 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 开发用例的注意事项:

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

单一的验证可以包含在 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();
    }
}

作为一位过来人也是希望大家少走一些弯路

在这里我给大家分享一些自动化测试前进之路的必须品,希望能对你带来帮助。

(软件测试相关资料,自动化测试相关资料,技术问题答疑等等)

相信能使你更好的进步!

点击下方小卡片

相关推荐
娃哈哈_14 分钟前
基于Testng + Playwright的H5自动化巡检工具
测试开发·测试工具·自动化·html5·可用性测试·testng·playwright
2401_8576009517 分钟前
SpringBoot框架的企业资产管理自动化
spring boot·后端·自动化
z千鑫19 分钟前
【人工智能】利用大语言模型(LLM)实现机器学习模型选择与实验的自动化
人工智能·gpt·机器学习·语言模型·自然语言处理·自动化·codemoss
zhd15306915625ff19 分钟前
化工厂主要涉及的自动化备件有哪些?
运维·自动化·化工厂
Jason-河山19 分钟前
利用API返回值实现商品信息自动化更新:技术与实践
运维·自动化
_.Switch5 小时前
高级Python自动化运维:容器安全与网络策略的深度解析
运维·网络·python·安全·自动化·devops
安冬的码畜日常5 小时前
【玩转 Postman 接口测试与开发2_006】第六章:Postman 测试脚本的创建(中):脚本的位置与执行顺序、AI助手及私有模块的使用
测试工具·postman·测试脚本·postbot·package library
IT规划师6 小时前
开源 - Ideal库 - 常用时间转换扩展方法(二)
开源·.net core·时间转换·ideal库
_tison7 小时前
夜天之书 #103 开源嘉年华纪实
开源
customer088 小时前
【开源免费】基于SpringBoot+Vue.JS周边产品销售网站(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·java-ee·开源