Selenium之Page Object模式

Page Object Model(POM)方案发展史

PageObject模式

  • 做法
    • 以页面未单位独立建模
    • 隐藏实现细节
    • 本质面向接口编程
  • 优点
    • 减少重复find click样板代码
    • 易读性提高
    • 页面修改不影响测试用例

Image From Martin Fowler

Image From Martin Fowler

Selenium 正式引入

2015年3月,selenium正式引入PageObjects模式

官网介绍PageObject模式

Page Object Model的基本原则

Summary

  • The public methods represent the services that the page offers
  • Try not to expose the internals of the page
  • Generally don’t make assertions
  • Methods return other PageObjects
  • Need not represent an entire page
  • Different results for the same action are modelled as different methods

PageObject模式原则解读

  • 方式意义
    • 用公共方法代表UI所提供的功能
    • 方法应该返回其他的PageObject或者返回用于断言的数据
    • 同样的行为不同的结果可以建模为不同的方法
    • 不要在方法内加断言
  • 字段意义
    • 不要暴露页面内部的元素给外部
    • 不需要建模UI内的所有元素

场景示例说明:登陆场景

  • 登陆页面提供login findPassword功能
    • Login类+login findPassword方法
  • 登录页面内的元素有多少并不关心,隐藏内部界面控件
  • 登录成功和失败会分别返回不同的页面
    • findPassword
    • loginSuccess
    • loginFail
  • 通过方法返回值判断登录是否符合预期

Java Python的封装方法

各个语言的binding都实现了po的简单封装,但是使用效果不好,需要自己定制

基于POM的用例组织结构

  • page:完成对页面的封装
  • testcase:调用各类page完成业务流程并进行短验
  • data:配置文件和数据驱动
  • utils:其他便捷的功能封装

编写用例顺序

  • 根据界面封装PO类方法,实现暂时设置为空
  • 编写用例,明确po里方法的入参、返回值、断言
  • 实现po内的方法,与自动化框架开始结合
  • 调试
  • 整体类似TDD风格

原生Selenium PO模式

框架默认PO定位策略的不足

  • UI的控件定位有复杂性,需要自定义
    • 动态加载的UI,可以找到但是位置可能发生变动
    • 动态加载的空间可能会获取到最早的默认值
    • 动态出现一些tips需要特殊处理
  • 改进(修改较为麻烦,需要对底层深入理解)
    • 自定义find方法,更灵活的find行为定义封装
    • 改进默认的注解

不推荐使用原生PO支持

  • 真是的情况更复杂,原生PO支持方法不足以应对
  • 不容易定制,比如Java注解,维护注解需要较高的成本
  • 多数公司都在使用相同的Page Object思想进行自定义封装

实战说明

企业微信web版进行自动化案例演示

企业微信地址:https://work.weixin.qq.com/wework_admin/frame

注意:企业微信登录需要扫二维码登录

Cookie登录

  1. 登录企业微信后,获取Cookie(获取Cookie的方式在另一篇文章介绍,具体可看该文章),由于扫二维码必须由人工介入,登录只能是Cookie的方式

    image-20201130163232013

  2. 处理获取的Cookie,处理成Driver需要的格式

    • 复制第一步获取到的cookie,使用awk处理成我们想要的格式(awk方式只是之一,也可以使用其他的方式)

      image-20201130163909047

  3. 编写测试代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    package com.auto.demo.web;

    import org.junit.jupiter.api.Test;
    import org.openqa.selenium.Cookie;
    import org.openqa.selenium.chrome.ChromeDriver;

    /**
    * @author jingLv
    * @date 2020/11/30
    */
    public class TestWeWork {

    @Test
    void openChrome(){
    String url = "https://work.weixin.qq.com/wework_admin/frame";
    ChromeDriver driver = new ChromeDriver();
    driver.get(url);

    driver.manage().addCookie(new Cookie("pgv_pvid", "965326920"));
    driver.manage().addCookie(new Cookie("wwrtx.c_gdpr", "0"));
    driver.manage().addCookie(new Cookie("pac_uid", "0_aee823a5acb34"));
    driver.manage().addCookie(new Cookie("wwrtx.i18n_lan", "zh"));
    driver.manage().addCookie(new Cookie("_ga", "GA1.2.486968943.1606468059"));
    driver.manage().addCookie(new Cookie("wwrtx.ref", "direct"));
    driver.manage().addCookie(new Cookie("wwrtx.refid", "4250759204197565"));
    driver.manage().addCookie(new Cookie("wwrtx.ltype", "1"));
    driver.manage().addCookie(new Cookie("wxpay.corpid", "1970324947149288"));
    driver.manage().addCookie(new Cookie("wxpay.vid", "1688850129600022"));
    driver.manage().addCookie(new Cookie("wwrtx.vid", "1688850129600022"));
    driver.manage().addCookie(new Cookie("ww_rtkey", "1166bq8"));
    driver.manage().addCookie(new Cookie("Hm_lvt_9364e629af24cb52acc78b43e8c9f77d", "1606468059,1606468075,1606469576,1606714819"));
    driver.manage().addCookie(new Cookie("Hm_lpvt_9364e629af24cb52acc78b43e8c9f77d", "1606714819"));
    driver.manage().addCookie(new Cookie("_gid", "GA1.2.1004022239.1606714820"));
    driver.manage().addCookie(new Cookie("wwrtx.d2st", "a532885"));
    driver.manage().addCookie(new Cookie("wwrtx.sid", "6QnZz9M3D9-XwaqXVRhlplYnrydnCorqqH61w8jMbhRbcTni27ASNi5GwQRoRc5t"));
    driver.manage().addCookie(new Cookie("wwrtx.vst", "ls7NqhOyvl2ER2hxI3QOsG8TBvjH-uVP7U8ymdRREjZQ5t5Nagz1nbTVjEPlAmowyB1hK90UquW63nmuOgtSNryFZYa916K9L8enRAa-RMt73xWiFHBYjCToPxoNsUD38YMpiVYPLsLdTrlR0oIuBSZmznOonOYr8i7W5KoCVfyeV6QK0QesguC5GqNjDGq9btyGaDpKJr18dE_j-Hl0FrVmGMig4VJhv_UKNHEYKQtTEsu3uFCfTquGCh1VUZZQfxs2w_aLM4_5flndp7EVEg"));

    driver.get(url);
    }
    }

业务流程案例

实现企业微信的登录后,可进行以下的流程的测试:

  • 通讯录增加成员
  • 通讯录搜索成员
  • 通讯录删除成员

根据以上需求,以PO的思想模式,进行通讯录页面的封装(演示简单的流程操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.auto.demo.web.page;

import org.openqa.selenium.By;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

/**
* "通讯录"PO页面
*
* @author jingLv
* @date 2020/11/30
*/
public class ContactPage {

/**
* 新增通讯录成员
*
* @param username
* @param acctId
* @param mobile
* @return
*/
public ContactPage addMember(String username, String acctId, String mobile) {
By addMember = By.linkText("添加成员");
// 显示等待,等待控件可见,问题:控件可见,但不一定可点,则在学要进行显示等待判断
new WebDriverWait(MainPage.driver, 10).until(ExpectedConditions.visibilityOfElementLocated(addMember));
// 显示等待,等待控件是否可点,问题:就算可点,仍有一定的概率点击不成功
new WebDriverWait(MainPage.driver, 10).until(ExpectedConditions.elementToBeClickable(addMember));

// 增加循环进行点击,保证稳定性,只要控件存在则一直点击(死循环)
while (MainPage.driver.findElements(addMember).size() > 0) {
MainPage.driver.findElement(addMember).click();
}
MainPage.driver.findElement(By.name("username")).sendKeys(username);
MainPage.driver.findElement(By.name("acctid")).sendKeys(acctId);
MainPage.driver.findElement(By.name("mobile")).sendKeys(mobile);
// 判断【保存】按钮是否存在
while (MainPage.driver.findElements(By.cssSelector(".js_btn_save")).size() > 0) {
MainPage.driver.findElement(By.cssSelector(".js_btn_save")).click();
}
return this;
}

/**
* 查找通讯录成员
*
* @param keywords
* @return
*/
public ContactPage search(String keywords) {
MainPage.driver.findElement(By.id("memberSearchInput")).sendKeys(keywords);
new WebDriverWait(MainPage.driver, 10).until(ExpectedConditions.elementToBeClickable(By.className("member_display_cover_detail_name")));
new WebDriverWait(MainPage.driver, 10).until(ExpectedConditions.elementToBeClickable(By.linkText("删除")));
return this;
}

/**
* 删除通讯录成员
*
* @return
*/
public ContactPage delete() {
MainPage.driver.findElement(By.linkText("删除")).click();
new WebDriverWait(MainPage.driver, 10).until(ExpectedConditions.elementToBeClickable(By.linkText("确认")));
MainPage.driver.findElement(By.linkText("确认")).click();
return this;
}

}

注意:元素等待的问题,使用了显示等待后,依旧会有部分元素不可操作,因此为了增加稳定性,使用了循环进行控制,该循环是死循环因此又出现了问题,当元素真的不存在时,死循环就会不停的查找操作,会导致流程陷入该步骤的死循环中,不会后续的步骤进行,后续会继续优化

现在简单的封装了”通讯录“的页面,但是访问”通讯录“的提前是需要进登陆操作的,因此在这个基础上简单封装一个主页面进行Cookie登录和WebDriver的声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
package com.auto.demo.web.page;

import org.openqa.selenium.Cookie;
import org.openqa.selenium.chrome.ChromeDriver;

import java.util.concurrent.TimeUnit;

/**
* 首页页面
*
* @author jingLv
* @date 2020/11/30
*/
public class MainPage {

public static ChromeDriver driver;

public MainPage() {
String url = "https://work.weixin.qq.com/wework_admin/frame";
driver = new ChromeDriver();
driver.manage().timeouts().implicitlyWait(5, TimeUnit.SECONDS);
driver.get(url);

driver.manage().addCookie(new Cookie("pgv_pvid", "965326920"));
driver.manage().addCookie(new Cookie("wwrtx.c_gdpr", "0"));
driver.manage().addCookie(new Cookie("pac_uid", "0_aee823a5acb34"));
driver.manage().addCookie(new Cookie("wwrtx.i18n_lan", "zh"));
driver.manage().addCookie(new Cookie("_ga", "GA1.2.486968943.1606468059"));
driver.manage().addCookie(new Cookie("wwrtx.ref", "direct"));
driver.manage().addCookie(new Cookie("wwrtx.refid", "4250759204197565"));
driver.manage().addCookie(new Cookie("wwrtx.ltype", "1"));
driver.manage().addCookie(new Cookie("wxpay.corpid", "1970324947149288"));
driver.manage().addCookie(new Cookie("wxpay.vid", "1688850129600022"));
driver.manage().addCookie(new Cookie("wwrtx.vid", "1688850129600022"));
driver.manage().addCookie(new Cookie("ww_rtkey", "1166bq8"));
driver.manage().addCookie(new Cookie("Hm_lvt_9364e629af24cb52acc78b43e8c9f77d", "1606468059,1606468075,1606469576,1606714819"));
driver.manage().addCookie(new Cookie("Hm_lpvt_9364e629af24cb52acc78b43e8c9f77d", "1606714819"));
driver.manage().addCookie(new Cookie("_gid", "GA1.2.1004022239.1606714820"));
driver.manage().addCookie(new Cookie("wwrtx.d2st", "a532885"));
driver.manage().addCookie(new Cookie("wwrtx.sid", "6QnZz9M3D9-XwaqXVRhlplYnrydnCorqqH61w8jMbhRbcTni27ASNi5GwQRoRc5t"));
driver.manage().addCookie(new Cookie("wwrtx.vst", "ls7NqhOyvl2ER2hxI3QOsG8TBvjH-uVP7U8ymdRREjZQ5t5Nagz1nbTVjEPlAmowyB1hK90UquW63nmuOgtSNryFZYa916K9L8enRAa-RMt73xWiFHBYjCToPxoNsUD38YMpiVYPLsLdTrlR0oIuBSZmznOonOYr8i7W5KoCVfyeV6QK0QesguC5GqNjDGq9btyGaDpKJr18dE_j-Hl0FrVmGMig4VJhv_UKNHEYKQtTEsu3uFCfTquGCh1VUZZQfxs2w_aLM4_5flndp7EVEg"));

driver.get(url);
}

/**
* 调用通讯录页面
*
* @return
*/
public ContactPage toContact() {
// 进入通讯录页面
driver.findElementByCssSelector("#menu_contacts").click();
return new ContactPage();
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.auto.demo.web.testcase;

import com.auto.demo.web.page.ContactPage;
import com.auto.demo.web.page.MainPage;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

/**
* @author jingLv
* @date 2020/11/30
*/
public class TestContact {

private static MainPage mainPage;
private static ContactPage contactPage;

@BeforeAll
static void start() {
mainPage = new MainPage();
contactPage = mainPage.toContact();
}

@Test
void testAddMember() {
contactPage.addMember("xiaohei", "0016", "13612343245");
}

@Test
void testSearch() {
contactPage.search("xiaohei").delete();
}

@AfterAll
static void close() {
MainPage.driver.quit();
MainPage.driver = null;
}
}

代码示例github地址:https://github.com/jinglv/selenium-for-java