PageObject(PO)设计模式在 UI 自动化中的实践总结(以 QQ 邮箱登陆为例)

简介: PageObject(PO)设计模式在 UI 自动化中的实践总结

1. 认识 PageObject

1.1 PO首次提出

PO的思想最早是2013年由IT大佬Martin Flower提出的:
https://martinfowler.com/bliki/PageObject.html
没错,就是他

  1. image.png

--- 没错,就是他 ---

在他的文章里有这样一张经典样图,图片中展示了测试代码中直接操作HTML元素和使用PO模式将page对象封装成一个HTML页面,通过特定方法来操作元素的对比;如下图:
image.png

1.2 PO原则解读

我们知道,PO主要就是应用在UI自动化测试上(Web端和App端均适用),因此2015年,Selenium官方给出了PO的设计原则说明:https://github.com/SeleniumHQ/selenium/wiki/PageObjects
image.png

对官方的原则进行解读,我们可以得到如下的信息:

1.2.1 方法意义

  • 用公共方法代表UI所提供的功能

如企业微信的通讯录页面,其中有“添加成员”、“批量导入,导出”、“设置所在部门”、“删除”等功能,这些功能都可以封装成通讯录这个UI界面所提供的方法;当然,部分数据较多或者较为复杂,复用性也比较高的话,例如添加成员,也可以单独抽离出来做一个page。
image.png

  • 方法应该返回其他的PageObject或者返回用于断言的数据

我们既然以页面为对象进行业务操作,那么一个方法结束后必然要有返回值:

要么返回一个页面,这个页面可以是当前页(因为可能还要在这个页面进行其他操作),可以是其他页面(我们操作某个方法后很可能会跳转到另一个页面进行下一步操作);

要么返回需要断言的值,测试用例总归有预期结果的对吧,那么最后肯定要有方法返回一个值,用来给我们做断言,来判断用例执行是否符合预期结果。

不要返回null或者写一个void没有返回值的方法,这样的方法没有意义,既不能为下一步操作创造条件,也不能为用例的断言提供结果。

  • 同样的行为不同的结果可以建模为不同的方法

这个就比较好理解了,拿最简答的登录场景来说:

同样的行为: 无论输入的账号密码正确与否,都是按照输入账号密码,点击登录这样的行为去操作

不同的结果:账号密码错误和正确得到的登录响应一定是不同的。

建模为不同的方法:对于登录页来说,就可以根据登录信息正确与否建模出正确登录、账号错误登录、密码错误登录等方法了

  • 不要在方法内加断言

对一个测试用例的执行结果进行判断一定是在测试用例里的,方法只是提供给我们业务上需要的操作,因此断言不要加在方法里,而是应该写在用例里

1.2.2 字段意义

  • 不要暴露页面内部的元素给外部

我们使用PO的目的就是为了提高测试用例的可读性和可维护性,只要我们人能操作的事,通过page对象封装好的客户端都可以做到;就类似于一个接口,我们只关心请求操作后接口的返回值是什么,而不需要关心接口内部到底是如何工作的

  • 不需要建模UI内的所有元素

一个UI页面可能会包含很多的元素,但是我们只要根据实际业务需求,将我们用的上的元素进行建模即可

1.3 PO的做法和优点

1.3.1 PO的做法总结

  • 以页面为单位独立建模
  • 隐藏实现细节
  • 本质是面向接口编程

1.3.2 基于POM的用例组织结构

  • page :完成对页面的封装
  • driver :完成对Web、Android、Ios、接口的驱动
  • testcase :调用各类page完成业务流程并进行断言
  • data :配置文件和数据驱动
  • utils :其他便捷的功能封装(可选)

1.3.3 PO的优点

  • 减少例如find click这类样板代码的重复
  • 测试用例的可读性提高,只关心业务流程
  • 测试用例可维护性提高,UI页面频繁被修改了,我们只需要去修改对应PO即可,用例无需修改

2、PO封装演示

说的再多,不如动手,下面以QQ邮箱登录为例,演示PO模式在UI自动化中的应用

2.1 登录场景预设

  • 登录页面提供login功能——LoginPage类+login方法
  • 登录页面内有多少元素并不关心,隐藏内部细节
  • 登录成功和失败会返回不同的页面
  • loginSuccess——MainPage(进入主页面)
  • loginFail——LoginPage(停留在登录页)
  • 通过方法返回值判断登录是否符合预期

2.2 代码实现

1)创建基础类BasePage,初始化driver,并封装常用的元素操作方法,如click、sendKeys等

package poshow.page;

import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;

import java.util.List;

public class BasePage {

    public static WebDriver driver;


    public WebElement findElement(By by){
        return driver.findElement(by);
    }

    public List<WebElement> finElements(By by){
        return driver.findElements(by);
    }

    public void click(By by){
       findElement(by).click();
    }

    public void sendKeys(By by,String context){
        findElement(by).sendKeys(context);
    }

    public String getText(By by){
        return findElement(by).getText();
    }
}

2)创建MainPage类,用于登录成功后的返回页面,由于这里并未演示登录后的操作,所以类中无具体方法实现,仅作为loginSuccess后的返回对象

package poshow.page;

public class MainPage extends BasePage{
}

3)创建LoginPage类,继承BasePage类。定义所需元素定位方式并根据操作动作(输入账号、输入密码、点击登录)将其封装成具体的业务操作方法,例如登录成功,用户名错误登录、密码错误登录等,输入的测试数据作为方法的入参传入(username,password)

package poshow.page;

import org.openqa.selenium.By;
import org.openqa.selenium.chrome.ChromeDriver;
import java.util.concurrent.TimeUnit;

public class LoginPage extends BasePage{
    //定位器
    By usernameInput = By.name("u");  //获取用户名输入框
    By passwordInput = By.id("p");    //获取密码输入框
    By submitLogin = By.cssSelector("#login_button"); //获取登录按钮
    By ErrM = By.id("err_m");  //获取错误提示信息


    public void openUrl(){
        String url = "https://mail.qq.com/";
        driver = new ChromeDriver();
        driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
        driver.get(url);
        driver.manage().window().maximize();
        driver.switchTo().frame("login_frame");

    }

    private void sleepWait(){
        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    //业务方法

    /*
    登录方法
     */
    private void login(String username,String password){
        findElement(usernameInput).clear();
        findElement(passwordInput).clear();
        sendKeys(usernameInput,username);
        sendKeys(passwordInput,password);
        click(submitLogin);
    }

    /*
      成功登录
     */
    public MainPage loginSuccess(String username,String password){
        login(username,password);
        return new MainPage();
    }


    /*
     密码错误登录
     message:你输入的帐号或密码不正确,请重新输入。
     */
    public String loginWithErrPassword(String username,String password ){
        login(username,password);
        sleepWait();
        return getText(ErrM);
    }

    /*
    账号为空登录
    你还没有输入帐号!
     */
    public String loginWithErrUsername(String username,String password){
        login(username,password);
        sleepWait();
        return getText(ErrM);

    }

    /*
    密码为空登录
     */
    public String loginWithoutPassword(String username,String password){
        login(username,password);
        sleepWait();
        return getText(ErrM);
    }
}

4)最后创建LoginTest测试类,编写测试用例;用例的编写更接近于人的行为,人想要登录邮箱,只需要依靠用户名和密码完成登录的行为即可,无需关注具体的输入框和登录按钮是如何定位,如何进行输入点击的。并在用例中加入断言进行判断。

package poshow.testcase;

import org.junit.jupiter.api.*;
import poshow.page.LoginPage;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class LoginTest {

    LoginPage loginPage = new LoginPage();

   @BeforeAll
   static void openUrl(){
        new LoginPage().openUrl();
    }

    @Test
    @DisplayName("密码错误登录")
    @Order(1)
    void loginWithErrPassword(){
        String username = "376057520";
        String password = "123456";
        String expectedErrM = "你输入的帐号或密码不正确,请重新输入。";

        String errM = loginPage.loginWithErrPassword(username, password);
        assertThat(errM,equalTo(expectedErrM));
    }

    @Test
    @DisplayName("账号错误登录")
    @Order(2)
    void loginWithErrUsername(){
        String username = "111";
        String password = "123456";
        String expectedErrM = "请输入正确的帐号!";

        String errM = loginPage.loginWithErrUsername(username, password);
        assertThat(errM,equalTo(expectedErrM));
    }

    @Test
    @DisplayName("空密码登录")
    @Order(3)
    void loginWithoutPassword(){
        String username = "376057520";
        String password = "";
        String expectedErrM = "你还没有输入密码!";

        String errM = loginPage.loginWithoutPassword(username, password);
        assertThat(errM,equalTo(expectedErrM));
    }

    @Test
    @DisplayName("正确登录")
    @Order(4)
    void logSuccess(){
       String username = "376057520";
       String password = "xxx";
       loginPage.loginSuccess(username,password);
    }

}

5)整体结构展示:
image.png

2.3 运行效果

1.gif

3、补充说明

3.1 用例设计

  • case尽量保持独立
  • suite体系管理用例的顺序
  • 不要把大量的业务校验逻辑放到UI自动化测试里, UI主要校验的是用户交付,操作流程,样式、数据、兼容性。
  • 与接口测试合理的分工

3.2 补充说明

以上仅仅是为了演示PO而举的一个简单的demo,实际上还有很大的优化空间:

  • 常用元素操作方法可以进一步封装的更完善
  • 可封装常用的操作util类,例如滑动
  • 特定元素的等待采用显示等待
  • 登录用例可以利用参数化来以数据驱动的方式完成,使用例代码更简洁易懂
  • PO代码和testcase代码可以分开,test下只放case代码

等等~后续需要大家一起继续完善。

更多技术文章分享及测试资料点此获取

相关文章
|
3月前
|
设计模式 算法 搜索推荐
后端开发中的设计模式应用与实践
在软件开发的广袤天地中,后端技术如同构筑高楼大厦的钢筋水泥,支撑起整个应用程序的骨架。本文旨在通过深入浅出的方式,探讨后端开发领域内不可或缺的设计模式,这些模式犹如精雕细琢的工具箱,能够助力开发者打造出既健壮又灵活的系统架构。从单例模式到工厂模式,从观察者模式到策略模式,每一种设计模式都蕴含着深刻的哲理与实践价值,它们不仅仅是代码的组织方式,更是解决复杂问题的智慧结晶。
|
4月前
|
设计模式 算法 测试技术
PHP中的设计模式:策略模式的应用与实践
在软件开发的浩瀚海洋中,设计模式如同灯塔,指引着开发者们避开重复造轮子的暗礁,驶向高效、可维护的代码彼岸。今天,我们将聚焦于PHP领域中的一种重要设计模式——策略模式,探讨其原理、应用及最佳实践,揭示如何通过策略模式赋予PHP应用灵活多变的业务逻辑处理能力,让代码之美在策略的变换中熠熠生辉。
|
2月前
|
设计模式 算法 开发者
探索编程语言中的设计模式:从理论到实践
设计模式,这一编程世界中的灯塔,为无数开发者照亮了复杂问题解决的道路。本文将深入探讨设计模式在编程实践中的运用,以代码示例揭示其背后的智慧。无论你是初学者还是资深开发者,都能在这里找到启发和共鸣。让我们一起领略设计模式的魅力,开启编程世界的新篇章!
|
2月前
|
设计模式 监控 算法
Python编程中的设计模式应用与实践感悟###
在Python这片广阔的编程疆域中,设计模式如同导航的灯塔,指引着开发者穿越复杂性的迷雾,构建出既高效又易于维护的代码结构。本文基于个人实践经验,深入探讨了几种核心设计模式在Python项目中的应用策略与实现细节,旨在为读者揭示这些模式背后的思想如何转化为提升软件质量的实际力量。通过具体案例分析,展现了设计模式在解决实际问题中的独特魅力,鼓励开发者在日常编码中积极采纳并灵活运用这些宝贵的经验总结。 ###
|
2月前
|
设计模式 开发者 Python
Python编程中的设计模式应用与实践感悟####
本文作为一篇技术性文章,旨在深入探讨Python编程中设计模式的应用价值与实践心得。在快速迭代的软件开发领域,设计模式如同导航灯塔,指引开发者构建高效、可维护的软件架构。本文将通过具体案例,展现设计模式如何在实际项目中解决复杂问题,提升代码质量,并分享个人在实践过程中的体会与感悟。 ####
|
3月前
|
设计模式 API 持续交付
深入理解微服务架构:设计模式与实践
【10月更文挑战第19天】介绍了微服务架构的核心概念、设计模式及最佳实践。文章详细探讨了微服务的独立性、轻量级通信和业务能力,并介绍了聚合器、链式和发布/订阅等设计模式。同时,文章还分享了实施微服务的最佳实践,如定义清晰的服务边界、使用API网关和服务发现机制,以及面临的挑战和职业心得。
|
3月前
|
设计模式 存储 数据库连接
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发中,设计模式是提高代码可维护性、扩展性和复用性的关键技术之一。本文将通过探讨单例模式,一种最常用的设计模式,来揭示其在PHP中的应用及优势。单例模式确保一个类仅有一个实例,并提供一个全局访问点。通过实际案例,我们将展示如何在PHP项目中有效实现单例模式,以及如何利用这一模式优化资源配置和管理。无论是PHP初学者还是经验丰富的开发者,都能从本文中获得有价值的见解和技巧,进而提升自己的编程实践。
|
3月前
|
设计模式 算法 PHP
PHP中的设计模式:策略模式的深入解析与实践
【10月更文挑战第9天】 策略模式是一种行为设计模式,它允许在运行时选择算法的行为。在PHP开发中,通过使用策略模式,我们可以轻松切换算法或逻辑处理方式而无需修改现有代码结构。本文将深入探讨策略模式的定义、结构以及如何在PHP中实现该模式,并通过实际案例展示其应用价值和优势。
47 1
|
3月前
|
设计模式 算法 PHP
PHP中的设计模式:策略模式的深入解析与实践
在PHP开发中,设计模式是提高代码可读性、可维护性和扩展性的重要工具。本文将深入探讨策略模式这一行为型设计模式,通过分析其定义、结构、使用场景以及在PHP中的实际应用,帮助开发者更好地理解和运用策略模式来优化自己的项目。不同于传统摘要的简洁概述,本文摘要部分将详细阐述策略模式的核心理念和在PHP中的实现方法,为读者提供清晰的指引。
|
3月前
|
设计模式 存储 测试技术
PHP中的设计模式:单例模式的深入解析与实践
在PHP开发领域,设计模式是解决常见问题的最佳实践。本文将深入探讨单例模式,一种确保类只有一个实例的设计模式,并提供实际应用示例。我们将从单例模式的基本概念讲起,通过实际案例展示如何在PHP中实现单例模式,以及它在不同场景下的应用和优势。最后,我们会探讨单例模式的优缺点,帮助开发者在实际项目中做出明智的选择。

热门文章

最新文章