App 自动化测试中的 Page Object 模式。
在传统的 Web 自动化测试中,我们使用的是 self.driver.find 表达式。而对于移动端自动化测试,也会遵循这样的习惯,使用 self.driver.find_element_by_id 进行各种各样的操作。几乎 80% 以上的公司和初级测试工程师都在使用这样的方式来编写测试用例。
但是这个模式没有办法应对 UI 的变化,也没有办法清晰的表达业务用途,所以这时,我们就需要解决之前课程中讲过的几个问题:要适应 UI 的变化、减少样板代码、能够清晰的表达业务用例场景、能够更智能化的解决异常处理场景等。
PO 模式前面也已经讲过了,当时主要针对 Selenium,但是其实它是完全可以应用到 Appium 上的,因为无论是 Appium 还是 Selenium,都有 UI 端的自动化,它们的设计理念和所面临的问题,在本质上是一样的。
我们接下来看一下,针对移动端,应该如何做 PO 改造?
编写用例顺序
首先,目录结构我们仍然沿袭 Selenium 所使用的 page、testcase、data 以及 utils。但这次改造只需要用到 page 和 testcase。
第 2 步是编写用例的顺序,我们可以根据界面先了解用例的流程,然后封装对应 PO 的类,接着再去编写用例,明确 PO 中方法的入参、返回值和断言。注意,这个时候方法的实现仍然是空的。
第 3 步,整体流程已经明确,业务也比较清晰了,这个时候我们开始完成某一个业务具体方法内的自动化。相当于有了大体的框架后,再实现与 Selenium 或与 Appium 之间的对接。
最后一步是完成对应的调试,实现所有功能,以上是一种类似于 TDD 风格的写法。
App自动化测试
接下来,我们看看具体怎样实现 App 自动化测试,举个例子,我们可以封装对应的 page,完成对应方法的改造。因为对应的 page 非常多,所以我会封装出一些像 BasePage 这样的父类来完成对通用功能的封装。
再往下是 App 自动化测试:基于 PO 模式的用例,它大概是什么类型呢?比如说一个 TestDemo 类里面有 n 个的 case,其中有 setup 和 teardown,我们在 setup 里完成对 page 的初始化,在 case 里完成对 page 相关功能的调用和断言,最后退出 App。
默认 PO 框架缺点
Appium 也是支持 PO 模式的。Selenium 最早的时候有一个功能叫 PageFactory,是可以用于 PO 的
Appium 也支持这样的工厂模式,我们可以在对应的元素上面加注解,这是 Java 风格的实现,而 Python 库里面默认是没有这个功能的。
默认 PO 框架的定位策略其实是有缺点的,无法处理一些实际场景。比如雪球行情底部的按钮,当你进入页面时,它其实是在中间的位置,但想要点击时会发现这个位置是错的,因为在列表加载完成的时候,它的位置会动态变化。
这导致很多自定义的业务流程需要我们自己来进行封装,比如一些动态出现的弹框和 tips,或者是在默认定位策略不满足我们需求的情况下,都要进行定制。真实的情况比官方写的要更复杂,除此之外, Java 开发人员可能使用 Java 的注解,这个维护成本非常高,而 Python 又没有对应的机制。所以我建议你采用 PO 的思想来实现适合自己公司的定制化改造。
PO 实战演练
了解了关于 PO 的基础理论,接下来我们进入一个实战环节分析具体的案例。
继续对之前的代码进行 PO 改造。 先来熟悉一下我们的业务流程。首先打开 App 进行搜索,点击搜索栏后会进入一个新的页面,我们在该页面输入搜索的内容,比如搜索阿里巴巴,输入完后会跳转到搜索结果页。这个过程中会涉及多个页面,但是我们可以简单理解成两个页面:一个是首页,一个是搜索结果页,因为大部分流程是在这两个页面完成的。
基于这个理解我们设计出两个 PO,把已有的 case 转化成基于 PO 的 case。首先我们创建一个 page 包,在 page 里完成对基础的 appActivity 界面对象的封装。进入 App 后首先进入首页,我们把首页命名为 main,在其中创建 Class MainPage()。在 MainPage 中涉及进入搜索的过程,所以我们可以在这加一个 search, 此时我们在首页点击时是没有输入任何内容的,它会直接跳到搜索页面,基于这个特点。我可以在这里保留一个 search 方法。
search 完后会进入另外一个页面,所以接下来我们需要 return 一个新的 SearchPage。SearchPage我们定义到 search.py里, 这样 MainPage 和 SearchPage 这两个类就创建好了。
那么 search 里面有什么样的内容?search 要完成一次搜索,需要在搜索栏输入关键词,然后再到结果页,所以它里面应该有一个叫 def search(self,) 的方法。因为 search 里有输入的内容,所以我们需要加一个叫 keyword 的参数。有了这个参数之后,我们就可以进入结果页,由于结果页仍然在这个页面里,所以返回值可以写成 return self。
还有一个功能是去断言它的股价,这时我们需要获取股价,所以需要一个类似于 get_price 的参数。get_price 根据什么来设置呢?假设有多个股价,我们要给定一个条件,目前我们默认直接取第 1 个。那么在 get_price 后要返回一个价格。我们假设股价是 100,就可以写成 return 100。这些逻辑你都可以先使用 pass 置空,或者简单的 return self,后面再去完成对它的实现。
这两个功能有了之后,我们再创建一个新用例 test_search ,我们在这里完成对阿里巴巴的搜索。
这个测试用例该如何去写呢?首先创建 MainPage, MainPage 需要初始化。初始化完成之后, MainPage 有个 search 功能,就可以进入搜索页,在搜索页可以再次进行搜索,比如输入阿里巴巴,搜完之后可以再继续进行搜索,也可以直接获取价格。拿到价格之后就可以断言股价是否大于 100,可能将来还有更多的 case,我们可以采用参数化的方法进行调节。
如果 MainPage 每一次都从首页进入,效率非常低,这时我们可以使用 setup 或者 setup_class 方法,setup_class 在整个过程中只会启动一次,而 setup 每次都会重新启动。在这我们先用最简单的 setup 方法给你做演示。
我们先把 MainPage 存储进来,输入 self.search_page,search_page 代表 MainPage 进入测试。后面的内容我们可以使用 self.search_page 来复用这个对象,从而完成对很多 case 场景的测试。
到这一步整个内容我们基本上已经写完了,如果后面有更多的内容我们可以使用参数化
最后还有一个 tearndown 需要去关闭,这里使用 self.search_page 参数。关闭 App 还要有一个全局性的操作,我们可以在 search_page 里增加一个 close 来执行这个操作。现在这个 close 方法还没有实现,需要在 search 里面完成一个关于 close 的改造。
现在这几个用例已经写完了,我们可以直接去运行它。所以在编写用例的时候首先需要确保你的基本 case 是流程是通顺的,虽然在运行时我们发现它提示类型不对,但至少到这一步,整个流程我们已经串起来了。