PO设计模式在 UI 自动化中的实践

本文介绍PO模式的基本概念及其在UI自动化测试中的应用。通过一个QQ邮箱登录的例子,详细展示了如何使用PO模式封装页面对象,实现测试代码的高可读性和可维护性。

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

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

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

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

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

  • 方法应该返回其他的PageObject或者返回用于断言的数据
    我们既然以页面为对象进行业务操作,那么一个方法结束后必然要有返回值:
    要么返回一个页面,这个页面可以是当前页(因为可能还要在这个页面进行其他操作),可以是其他页面(我们操作某个方法后很可能会跳转到另一个页面进行下一步操作);
    要么返回需要断言的值,测试用例总归有预期结果的对吧,那么最后肯定要有方法返回一个值,用来给我们做断言,来判断用例执行是否符合预期结果。

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

  • 同样的行为不同的结果可以建模为不同的方法
    这个就比较好理解了,拿最简答的登录场景来说:
    同样的行为: 无论输入的账号密码正确与否,都是按照输入账号密码,点击登录这样的行为去操作
    不同的结果:账号密码错误和正确得到的登录响应一定是不同的。
    建模为不同的方法:对于登录页来说,就可以根据登录信息正确与否建模出正确登录、账号错误登录、密码错误登录等方法了

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

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

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

  • 以页面为单位独立建模

  • 隐藏实现细节

  • 本质是面向接口编程

  • page :完成对页面的封装

  • driver :完成对Web、Android、Ios、接口的驱动

  • testcase :调用各类page完成业务流程并进行断言

  • data :配置文件和数据驱动

  • utils :其他便捷的功能封装(可选)

1.3.3 PO的优点

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

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

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

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)整体结构展示:

  • case尽量保持独立
  • suite体系管理用例的顺序
  • 不要把大量的业务校验逻辑放到UI自动化测试里, UI主要校验的是用户交付,操作流程,样式、数据、兼容性。
  • 与接口测试合理的分工 #### 3.2 补充说明 以上仅仅是为了演示PO而举的一个简单的demo,实际上还有很大的优化空间:
  • 常用元素操作方法可以进一步封装的更完善
  • 可封装常用的操作util类,例如滑动
  • 特定元素的等待采用显示等待
  • 登录用例可以利用参数化来以数据驱动的方式完成,使用例代码更简洁易懂
  • PO代码和testcase代码可以分开,test下只放case代码

更多技术文章

视频干货合集

2024最新Python3.1x软件测试开发必备语法基础讲解

7天软件测试快速入门教程

测试开发精品公开课合集

测试开发/自动化测试/性能测试/精准测试/测试左移/测试右移/人工智能测试

大厂面试真题解析

JMeter实时性能监控平台


另免费赠送软件测试开发方面的专业资料包!助您事半功倍,提升技能,把握职场先机。

软件测试职业发展
在这里插入图片描述
零基础入门
在这里插入图片描述

测试必备编程篇
在这里插入图片描述
自动化测试
在这里插入图片描述
性能测试
在这里插入图片描述
测试管理
在这里插入图片描述
工程效能篇
在这里插入图片描述
面试求职篇

软件测试的面试宝典,内含一线互联网大厂面试真题、面试技巧、软件测试面试简历指导,免费领取!
在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值