当前位置: 首页 > news >正文

android中断言_PageObject设计模式在 UI 自动化中的实践(QQ 邮箱登陆为例)

1. 认识 PageObject

d93d4bf058a4f944b109212f2be3c42d.png

1.1 PO首次提出

PO的思想最早是2013年由IT大佬Martin Flower提出的:

https://martinfowler.com/bliki/PageObject.html没错,就是他

38e534a77dcafceceafd720ef5c1f658.png

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

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

e7884d9eb5c0f17505741ebe8ad80cdc.png

1.2 PO原则解读

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

8ef3fcb3b662726d601072755c850bec.png

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

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

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

d36394a4d2d96251b2081c1d2ef77fc7.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封装演示

d93d4bf058a4f944b109212f2be3c42d.png

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

2.1 登录场景预设

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

dc2e8afebdb72408c8b00e968c211bf3.png

2.3 运行效果

e72e56cdbcaf90723f8273b8a82805f3.gif

3、补充说明

d93d4bf058a4f944b109212f2be3c42d.png

3.1 用例设计

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

(文章来源于霍格沃兹测试学院)

相关文章:

  • 广西中专机器人应用与维护_工业机器人工程师|记工信部考证班结课啦!
  • spring项目中target项目是什么‘’_手把手教你搭建一个SSM项目
  • django 查询时间范围_Django集成OpenLDAP认证
  • thrift端口未被占用连接不上_跨语言RPC框架:Thrift的使用例子解析(包含完整项目源码)...
  • 学习python有什么就业前景_学习Python的就业前景如何呢?
  • 变更控制管理流程图_药品批发企业的变更控制管理(六)管理流程详解3
  • python twisted安装_图文详解python之twisted模块安装
  • mongodb数据备份原理_MongoDB 数据备份和恢复 --- MongoDB基础用法(七)
  • mysql存在则更新 并发_SQL Server与MySQL在“存在则更新,不存在则插入”并发处理上的一些差异。...
  • MySQL时间加20天_mysql日期加一个天数获得新的日期
  • mysql创建子用户代码_mysql 创办用户
  • mysql 条件查询view_C#mysql数据库查询根据条件显示dataGridView
  • 引用类型如何避免数据结构的无穷嵌套问题_「Rust巅峰之作」- 从来没有谁能把数据类型讲的如此透彻...
  • 写个函数计算所有传入参数相乘的结果(参数数量不定)_Python 函数的封装
  • vue调用百度地图api_Vue Baidu Map 可能是 Vue 接入百度地图 API 的最佳组件了
  • AngularJS指令开发(1)——参数详解
  • ES学习笔记(12)--Symbol
  • gf框架之分页模块(五) - 自定义分页
  • Github访问慢解决办法
  • JavaScript类型识别
  • js数组之filter
  • Lsb图片隐写
  • python3 使用 asyncio 代替线程
  • v-if和v-for连用出现的问题
  • vue+element后台管理系统,从后端获取路由表,并正常渲染
  • 阿里研究院入选中国企业智库系统影响力榜
  • 程序员该如何有效的找工作?
  • 分布式事物理论与实践
  • 精彩代码 vue.js
  • 码农张的Bug人生 - 初来乍到
  • 让你的分享飞起来——极光推出社会化分享组件
  • 数据库写操作弃用“SELECT ... FOR UPDATE”解决方案
  • 职业生涯 一个六年开发经验的女程序员的心声。
  • k8s使用glusterfs实现动态持久化存储
  • 专访Pony.ai 楼天城:自动驾驶已经走过了“从0到1”,“规模”是行业的分水岭| 自动驾驶这十年 ...
  • ​【已解决】npm install​卡主不动的情况
  • ​3ds Max插件CG MAGIC图形板块为您提升线条效率!
  • $.ajax()
  • (1综述)从零开始的嵌入式图像图像处理(PI+QT+OpenCV)实战演练
  • (剑指Offer)面试题41:和为s的连续正数序列
  • (免费领源码)python+django+mysql线上兼职平台系统83320-计算机毕业设计项目选题推荐
  • (三)终结任务
  • (译) 理解 Elixir 中的宏 Macro, 第四部分:深入化
  • * CIL library *(* CIL module *) : error LNK2005: _DllMain@12 already defined in mfcs120u.lib(dllmodu
  • .net oracle 连接超时_Mysql连接数据库异常汇总【必收藏】
  • .net/c# memcached 获取所有缓存键(keys)
  • .NET企业级应用架构设计系列之应用服务器
  • .NET实现之(自动更新)
  • /etc/sudoer文件配置简析
  • @GetMapping和@RequestMapping的区别
  • @TableLogic注解说明,以及对增删改查的影响
  • [ vulhub漏洞复现篇 ] Django SQL注入漏洞复现 CVE-2021-35042
  • [20171106]配置客户端连接注意.txt
  • [BZOJ 4129]Haruna’s Breakfast(树上带修改莫队)
  • [C++]模板与STL简介