PageObject 设计模式
简介
首先我们来看一个案例,当我们去写一个 UI 自动化测试代码的时候,是不是经常遇到这样一个问题?就是我们的这个断言和我们的这个操作会混在一起。随着脚本的增多,这样维护起来越发痛苦。于是在 2013 年,有一个人叫什么叫 Martin flower 他提出了一个概念,就是这个 PageObject。时间来到了 2015 年,selenium 开始在官方文档中介绍有关 PageObject 的内容。好,我们来一下,这个 PageObject 模式,究竟是个什么样的东西,可以帮我们解决这个问题,我们先看一下作者官方文档。这个就是我们作者的博客文档,它是 2013 年提出一个概念叫 PageObject。它简述了一个什么概念呢?它说,这个段文字主要表达了要对一些细节操作进行一个封装。然后在调用的时候,只需要调用提供的这个接口就可以了,我们不用去关心这些操作细节。
然后我们看一下这个图,这个图片就说明了作者这个主要思想什么思想。我们看上面是封装出来的接口,比如说我想去选取一个相册的标题,把它们当成一个完整功能,就是 selectAlbumWithTitle()。那么还有其他的功能,例如 getArtist 获取作者,updateRating 更新排名。那么对应的操作细节是什么?就是下面的这些具体步骤,翻译过来就是,第一步选取这个相册,第二步选取标题,第三步获取文本,第四步点击,第五步获取排名字段。第六步设置内容。这就是操作细节。可以看到上面只是把这个操作细节给它包抽象出来称为一个方法,只是抽象出一个方法,它并不有并不包含操作细节,这个思想至关重要,它把具体的操作细节封装成了一个公共方法。这张图就很好地概括了作者一个核心思想,就是 PageObject 的封装。
传统 UI 自动化的问题
- 无法适应 UI 频繁变化
- 无法清晰表达业务用例场景
- 大量的样板代码 driver/find/click
那我们现在来总结一下这个传统 UI 自动化测试用例的一个问题。就是首先第一大最严重的一个问题就是它是没有办法适应我们 UI 的整体的变化的。我们的 UI 一旦发生变化,就会导致大量的 case 去需要修改。还要分别找出来所有使用旧的定位方式的代码,然后把每个用例都逐个去修改这个定位。那第二个很明显的问题,就是传统线性脚本,是没有办法去清晰的表达我们业务用例场景的。大家可以看一看我们的传统脚本,它中间就是跳转了几个页面,做了什么样的一个操作,干了一些什么事儿吗?如果没有注释,是很难看出来的。那这个原因是什么呢?因为我们这个封装是存在问题的,只能看到各种元素的属性。那接下来我们再来看第三条,它有大量的这个样板代码,同样以这份脚本为例,可以看到,不是在 find、click 就是在 find 和 click 的路上。那我们接下来如果说我们要改善这一些问题,让我们代码的可读性更强。答案就是我们今天的主角:PageObject 设计模式。
PageObject 模式的优势
- 降低 UI 变化导致的测试用例脆弱性问题
- 让用例清晰明朗,与具体实现无关
在 web 自动化测试中,Page Object 模式是一种常用的设计模式,其主要优势如下:第一点,降低 UI 变化导致的测试用例脆弱性问题:Page Object 将页面封装成对象,使测试用例与具体实现无关,当 UI 发生变化时,只需要修改 Page Object 中的实现细节,而测试用例本身不需要进行修改,从而避免了由 UI 变化带来的测试用例脆弱性问题。第二点:让用例清晰明朗,与具体实现无关 Page Object 将页面封装成对象,使测试用例可以直接调用 Page Object 中的方法来实现对页面元素的操作,使得用例更加清晰明朗,更容易理解和维护。同时,Page Object 将页面元素与测试用例的具体实现分离开来,使得测试用例可以关注于业务逻辑,而不需要关注页面元素的具体实现细节。总之,Page Object 模式使得 web 自动化测试更加稳定、可靠、可维护,提高了测试用例的可读性和可维护性,使得测试工作更加高效和有效。
PageObject 建模原则
PO 设计模式 6 大原则
- 字段意义
- 不要暴露页面内部的元素给外部
- 不需要建模 UI 内的所有元素
- 方法意义
- 用公共方法代表 UI 所提供的功能
- 方法应该返回其他的 PageObject 或者返回用于断言的数据
- 同样的行为不同的结果可以建模为不同的方法
- 不要在方法内加断言
这里列出了六大原则,它们是哪儿来的呢?它其实是根据 Martin flower 的博客,从他的理念中抽取出来的六大原则,这是给我们的一个很好的建议,我们可以进行一个参考。六大原则说了什么事呢?1. 不要暴露页面内部的元素给外部2. 不要建模 UI 内的所有元素3. 用公共方法代表 UI 所提供的功能4. 方法应该返回其他的 PageObject,或者返回用于断言的数据5. 同样的行为,不同的结果可以建模为不同的方法6. 不要在方法内加断言7. 不要暴露页面内部的元素给外部
1.不暴露页面内部元素给外部
不要暴露很多细节,我们的这个方法内是可以有细节的,但是方法它一定是一个接口的形式。这些网页上的元素,抽象成 PageObject 的属性,在 python 中,我们可以设置成内部属性,通常属性名称前,用一个或者两个下划线开头,表示这是非公开的属性。
2.不要建模 UI 内的所有元素
我们不要把页面中的每个元素进行建模,如果说这个页面中有非常多的功能,同样可能有几千上万个页面元素。如果为整个页面都去建一个模,这样会非常的繁琐复杂,是很不现实的。我们要学会挑重点,只需要为重要的元素进行建模就好了。
3.用公共方法代表 UI 所提供的功能
我们公共方法应该去代替一个页面的服务,这个怎么理解呢?我们以登录场景为例,在登陆页面通常需要填写用户名,密码,然后点击登录,甚至有的还需要填写验证码。那么我们只需要让登录页面,提供一个登录方法,例如叫 login(),然后具体如何去输入用户名、输入密码、点击登录的动作封装在其中,后面我们只需要调用 login()方法就可以实现登录。
4.法应该返回其他的 PageObject,或者返回用于断言的数据
这个是什么意思呢?这个指的是我们页面的公共方法的返回值。在我们的 web 自动化测试过程中,当一个页面进行了一些操作之后,通常会跳到其他的页面,比如登录页面登录成功后,通常会跳到系统的欢迎页,或者首页。这就是一个典型的页面跳转。那么如果说,这个场景已经执行完毕了,我们需要获得一些页面的反馈信息,用来帮助我们判断当前页面是否正确展示了对应的元素信息,那么就可以暴露一个返回元素信息的方法,这就是所讲的,方法可以返回用于断言的数据。
5.同样的行为,不同的结果可以建模为不同的方法
一个页面可能会有相同的动作,还是以登录为例,有时候我们需要测试登录成功,也要测试登录失败,那么就要封装成两个不同的方法,两个方法分别代表登录成功和登录失败,而不是写在一个方法里。一个方法只代表一种特定结果。
6.不要在方法内加断言
这个原则要求我们,将页面功能和测试用例解耦,断言是测试用例该做的时候,一定要和业务逻辑分开。不能写在一起。
POM 使用方法
- 把元素信息和操作细节封装到 PageObject 类中
- 根据业务逻辑,在测试用例中链式调用
首先,我们的每个页面,应该遵循 PO 建模原则就行建模,构建 Page Object 类,以及为每个页面所提供的功能提供对应的方法,以及提取对应的元素作为 PageObject 的内部属性。通常一个业务场景或涉及多个页面,正好 PageObject 是支持返回其他页面的 PageObject,因此可以实现跳转,那么测试用例中就可以使用链式调用的方式,来将各个页面依次串联起来。
示例展示
- selenium 官网示例(Java):
https://www.selenium.dev/zh-cn/documentation/test_practices/encouraged/page_object_models/
selenium 官网示例提供了一个 Java 实现的 Page Object 模式的示例。它演示了如何将元素定位和操作封装到 Page Object 类中,并如何在测试用例中使用这些 Page Object 类。在示例中,作者首先定义了一个 PageObject 基类,用于封装常用的查找元素和操作方法,如查找元素、获取元素文本、输入文本等。然后,每个具体的 Page Object 类都继承了 PageObject 基类,并添加了具体的元素定位和操作方法。在测试用例中,作者使用 PageFactory 类来创建 Page Object 对象,然后通过这些对象来执行测试操作。测试用例中只需要关注具体的业务流程,而不需要关注元素的定位和操作细节,从而实现了测试用例与页面实现的分离。这个示例展示了 Page Object 模式的核心思想和具体实现方法,可以帮助测试工程师更好地理解和应用 Page Object 模式。
搜索场景:传统线性脚本(Python)
- 传统测试用例
# test_search.py
from selenium import webdriver
from selenium.webdriver.common.by import By
class TestSearch:
def test_search(self):
# 初始化浏览器
self.driver = webdriver.Chrome()
self.driver.get("https://xueqiu.com/")
self.driver.implicitly_wait(3)
# 输入搜索关键词
self.driver.find_element(By.NAME, "q").send_keys("阿里巴巴-SW")
# 点击搜索按钮
self.driver.find_element(By.CSS_SELECTOR, "i.search").click()
# 获取搜索结果
name = self.driver.find_element(By.XPATH, "//table//strong").text
# 断言
assert name == "阿里巴巴-SW"
这段代码是一个简单的基于 Selenium 的测试用例,用于在雪球网站中搜索关键词“阿里巴巴-SW”,并断言搜索结果是否正确。 代码中首先初始化了 Chrome 浏览器,并打开了雪球网站。然后使用 Selenium 提供的定位方法,在搜索框中输入关键词,点击搜索按钮,再通过 XPath 定位到搜索结果中的股票名称,并获取其文本值。最后使用断言来判断实际搜索结果是否等于期望值,如果不等则测试用例失败。 但是这个测试用例没有使用 Page Object 模式,直接在测试用例中使用了 Selenium 提供的定位方法,导致测试用例与页面实现耦合在一起,如果页面元素发生变化,就需要修改测试用例中的定位方法,导致测试用例脆弱性高。另外,如果需要在多个测试用例中重复使用相同的操作,也需要重复编写相同的代码,代码复用性差。
搜索场景:POM 脚本(Python)
- 股票页面 PageObject
# search_page.py
from selenium import webdriver
from selenium.webdriver.common.by import By
class SearchPage:
__INPUT_SEARCH = (By.NAME, "q")
__BUTTON_SEARCH = (By.CSS_SELECTOR, "i.search")
__SPAN_STOCK = (By.XPATH, "//table//strong")
def __init__(self):
self.driver = webdriver.Chrome()
self.driver.implicitly_wait(3)
self.driver.get("https://xueqiu.com/")
def search_stock(self, stock_name: str):
self.driver.find_element(*self.__INPUT_SEARCH).send_keys(stock_name)
self.driver.find_element(*self.__BUTTON_SEARCH).click()
name = self.driver.find_element(By.XPATH, "//table//strong").text
return name
搜索场景:POM 脚本(Java)
- 股票页面 PageObject
# search_page.py
public class demoTest {
static WebDriver driver;
@AfterAll
static void teardown(){
}
@BeforeAll
static void setup(){
driver = new ChromeDriver();
}
@Test
void demo(String stock_name ){
driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(10));
driver.get("https://xueqiu.com/")
driver.findElement(By.name("q") ).send_keys(stock_name);
driver.findElement(By.cssSelector(".search")).click();
String name = driver.findElement(By.Xxpath("//table//strong")).text
return name
}
}
这段代码实现了一个基于 Page Object 模式的 SearchPage 类,封装了搜索页面中的元素和操作。其中,类的内部使用了私有变量和私有方法,对外提供了一个公共方法 search_stock 来执行搜索操作,并返回搜索结果。使用 Page Object 模式后,测试用例中只需要调用 SearchPage 类的 search_stock 方法,并传入股票名称作为参数即可完成搜索操作,避免了直接使用 Selenium 提供的定位方法,降低了测试用例的耦合度和脆弱性。另外,使用 Page Object 模式还可以提高代码的可读性和可维护性,便于代码重构和扩展。如果页面元素发生变化,只需要在 SearchPage 类中修改对应的元素定位信息即可,不需要修改测试用例代码,大大降低了测试用例维护的难度。
搜索场景:测试用例(Python)
- PO 模式测试用例
# test_search.py
from onSelenium.fei.page_objects.search_page import SearchPage
class TestSearch:
def test_search(self):
text = SearchPage().search_stock("阿里巴巴-SW")
# 断言
assert "阿里巴巴-SW" == text
这段代码使用了 Page Object 模式的 SearchPage 类来实现搜索操作,测试用例中只需要调用 SearchPage 类的 search_stock 方法,并传入股票名称作为参数即可完成搜索操作。相对于没有使用 Page Object 模式的测试用例,这段代码的优点在于:1. 可读性更强:测试用例代码更加简洁明了,通过调用 Page Object 类的方法,可以让测试用例的目的更加清晰明确。2. 可维护性更高:如果页面元素发生变化,只需要在 Page Object 类中修改对应的元素定位信息即可,不需要修改测试用例代码,大大降低了测试用例维护的难度。3. 代码复用性更高:在多个测试用例中都需要进行搜索操作时,可以复用 SearchPage 类的 search_stock 方法,避免了重复编写相同的代码。
总结
总的来说,使用 Page Object 模式的测试用例具有更高的可读性、可维护性和代码复用性。