自动化测试(10) | Selenium Java 封装
WebDriver 封装
欢迎阅读WebDriver封装讲义。本篇讲义将会重点介绍Selenium WebDriver API的封装的概念和方法,以及使用封装进行自动化测试的设计。
WebDriver API 封装
封装的概念
从之前的讲义和学习中,我们知道,WebDriver API的调用以及自动化测试,我们也初步接触了线性测试、以及模块化自动化测试和数据驱动测试,那么回顾之前的内容,我们不只是可以利用WebDriver提供的一系列的定位符以便使用元素定位方法,我们这里开始尝试封装后调用。首先,我们从封装的概念开始。
封装是一个面向对象编程的概念,是面向对象编程的核心属性,通过将代码内部实现进行密封和包装,从而简化编程。
所谓“对象”,形象地说,我们可以把它理解为一块积木。设计积木的人需要设计积木的外观与形状,还有内部的材质。堆积木的人对于内部的材质并不关心,他们只需要根据不同的外观与形状来决定堆放的位置。因此,对于开发者而言,要设计面向对象的程序,同时会是两个迥然不同的身份:设计者与使用者。
先谈谈使用者。使用者的身份,就是利用已经提供给你的所有对象,根据需求,设计出自己需要实现的程序。就如堆积木的过程。这恰恰是面向对象编程的优势所在,那就是“对象的重用”。已经设计好的对象,可以被不同的使用者调用,这些功能既然已经实现,对于使用者而言,当然就免去了自己去设计的过程。正如堆积木那样,既然有了现成设计好的积木,使用者所要做的工作就是把这些积木最后组合起来,堆成不同的形状。WebDriver
所提供的类库,就是这样的积木。那么我们以下的操作将会基于上述的定位符进行定位操作。
前面说到对象好比是一个积木,设计者需要定义好这个积木的外观和形状,也要考虑积木内部的制作,例如选用的材质,以及是空心还是实心。如果将这个积木剖开来看,实际上该对象应分为内、外两层。由于使用者只关心外部的实现,因此设计者就需要考虑,哪些实现应暴露在外,哪些实现应隐藏于内。这就体现了对象的封装的思想。
简而言之,封装就是把原始和原生的方法进行再包装。将原始的代码用心的代码包装起来,通过对新代码的调用,来使用原始的代码的过程。
封装的好处
对Selenium进行封装的好处主要有如下三个方面:
- 使用成本低
- 不需要要求所有的测试工程师会熟练使用Selenium,而只需要会使用封装以后的代码
- 不需要对所有的测试工程师进行完整培训。也避免工作交接的成本。
- 测试人员使用统一的代码库
- 维护成本低
- 通过封装,在代码发生大范围变化和迁移的时候,不需要维护所有代码,只需要变更封装的部分即可
- 维护代码不需要有大量的工程师,只需要有核心的工程师进行封装的维护即可
- 代码安全性
- 对作为第三方的Selenium进行封装,是代码安全的基础。
- 对于任何的代码的安全隐患,必须由封装来解决,使得风险可控。
- 使用者并不知道封装内部的代码结构。
封装的目的
封装,最终为了实现自动化测试框架。在自动化测试领域,有一个经典的问题:“既然可以编写或者通过record & playback可以做一个脚本,那么为什么还需要自动化测试框架呢?”简而言之,就是为什么需要如此这般麻烦的设计和编写自动化测试框架,不是原本已经可以做自动化测试了么?
这个回答可以很“官方”,从维护性、重用性、安全性和成本等角度可以回答。
在这点,就好比是建造房子。在没有设计框架的基础上,我们或许可以建造一个楼房,最多也就三两层吧;但是对于一份经过完整设计的图纸,人们可以建造出高楼大厦。
封装,就是把基础的石头等建材,通过我们的个性化的方法,将地基可用而安全的搭建好。是自动化测试框架的基石。
自动化测试模型
自动化测试模型.png- 封装 Selenium 为 BoxDriver
- 在 测试用例中,实例化 BoxDriver,产生 bd 对象
- 使用 bd 对象,构造 业务模块的实例化对象,产生 common
- 使用 common 在测试用例中,构建测试步骤
- 使用数据驱动的外部数据,通过读取,进行测试
- 执行整个用例
使用封装进行自动化测试
封装技术
- 面向对象的编程
- 类:模板,设计
- 构造方法
- 成员变量
- 成员方法
- 类的实例化产生对象
- 实例化过程: new 关键字
- 实例化的方法: 类名(参数)--> 构造方法
- 实例化过程相当于 类执行了构造方法,产生了 this
- 实例化出来的对象,可以访问成员方法
- 封装:做一个模板,这个模板有webdriver所有的功能
- 封装 webdriver,自动化 WebUI 测试
- 封装 Appium,自动化 APP UI 测试
- 封装 自定义的 Web接口 Driver
- 其他公司的开源框架
封装示例
测试脚本:以下的脚本
-
找到一个指定输入框(selector),并且输入指定的字符(text)
type(selector, text)
不用在业务逻辑中,使用多次的
findElement(By.id(...))
public void type(String selector, String text) { WebElement we = this.locateElement(selector); we.clear(); we.sendKeys(text); }
-
找到一个可以点击的元素(selector),并且点击(click)
click(selector)
public void click(String selector) { this.locateElement(selector).click(); }
-
找到一个指定的frame,并且切换进去
switchToFrame(selector)
public void switchToFrame(String selector) { WebElement we = this.locateElement(selector); this.baseDriver.switchTo().frame(we); }
-
找到一个指定的select,并且通过index进行选择
selectByIndex(selector, index)
public void selectByIndex(String selector, int index) { WebElement we = this.locateElement(selector); Select s = new Select(we); s.selectByIndex(index); }
以上的代码是封装了locateElement()
的几种方法,在具体使用封装过的代码的时候,只需要简单的调用即可。接下来的重点,是介绍 locateElement(selector)
的封装方式。
- 查找元素:
findElement(By...)
- 支持各种的查找:8种方式都需要支持,必须通过
selector
显示出分类-
selector
中需要包含一个特殊符号 - 实例化 封装好的类的时候,需要约定好是什么特殊符号
- 强制性用
硬编码 hard code
来实例化,例如,
或者?
或者 其他非常用字符=>
- 或者,构造方法中,传递
this.byChar
- 强制性用
-
- 要把查找到元素的返回给调用的地方:必须要有返回值,类型是
WebElement
private WebElement locateElement(String selector) {
WebElement we;
// 如果定位符中 有 分隔符,那么就从分隔符处分成两段
// 第一段是By
// 第二段是真正的定位符
// 如果没有分隔符,就默认用 id 定位
if (!selector.contains(this.byChar)) {
// 用 id 定位
we = this.baseDriver.findElement(By.id(selector));
} else {
// 用 分隔符 分成两个部分
String by = selector.split(this.byChar)[0];
String value = selector.split(this.byChar)[1];
we = findElementByChar(by, value);
}
return we;
}
- 接下来的重点,是实现
findElementByChar(by, value)
private WebElement findElementByChar(String by, String value) {
WebElement we = null;
switch (by.toLowerCase()) {
case "id":
case "i":
we = this.baseDriver.findElement(By.id(value));
break;
case "css_selector":
case "css":
case "cssselector":
case "s":
we = this.baseDriver.findElement(By.cssSelector(value));
break;
case "xpath":
case "x":
we = this.baseDriver.findElement(By.xpath(value));
break;
case "link_text":
case "link":
case "text":
case "linktext":
case "l":
we = this.baseDriver.findElement(By.linkText(value));
break;
//TODO: other by type
}
return we;
}
使用上面的封装类,就需要指定特定的 selector
类型 | 示例(分隔符以逗号, 为例) |
描述 |
---|---|---|
id | "account" 或者 "i,account" 或者 "id,account" | 分隔符左右两侧不可以空格 |
xpath | "x,//*[@id="s-menu-dashboard"]/button/i" | |
css selector | "s,#s-menu-dashboard > button > i" | |
link text | "l,退出" | |
partial link text | "p,退" | |
name | "n,name1" | |
tag name | "t,input" | |
class name | "c,dock-bottom |
调用示例
void logIn(String account, String password) throws InterruptedException {
BoxDriver driver = this.baseDriver;
driver.type("account", account);
driver.type("password", password);
driver.click("submit");
// 点击登录按钮后,需要等待浏览器刷新
Thread.sleep(2000);
}
自动化的测试代码示例
@Test
public void test01Login() {
common.openPage();
common.logIn(member.getAccount(), member.getPassword());
expectedUrl = this.baseUrl + "sys/index.html";
Assert.assertEquals(driver.getCurrentUrl(), expectedUrl, "新用户登录失败");
common.logOut(lang);
}
封装需要注意的部分
-
方法有无返回值、和参数
-
如果有返回值:确保代码所有的路径都会有返回值
public WebElement getElement(){ if (xxx) { return yyy; } } // 上述代码会报错,如果xxx条件不符合,就没有返回值的路径了。
-
如果有参数,确保非值类型的参数是非空的 不是 null
public void type(Driver driver){ driver.xxx() } // 如果driver是null,那么xxx会直接报错。 NullPointerException错误异常
-
如果有较多的分支语句,需要注意 break的使用
public WebElement getElement(String selector){ switch(selector.toLowerCase()){ case "id": case "i": driver.findElement(By.id(xxx)); break; case "x": xxx } } // 注意需要填写break
在python中,不支持switch语句,python使用的是 if ... elif
def get_element(selector): if selector == "id" or selecor == "i": driver.find_element_by_id(xxx) elif selector == "x": xxx
-
-
封装中,WebDriver对象的传递
-
WebDriver应该是在封装的类中,唯一存在。
class AutomateDriver(){ public click(String selector){ WebDriver driver = new FirefoxDriver(); driver.... } public type(String selector, String text){ WebDriver driver = new FirefoxDriver(); driver.... } } // 上述代码中,会打开多个火狐浏览器 // 正确的做法:声明一个全局的类的成员变量,进行赋值使用。
-
关于Firefox Profile的使用
在做自动化测试的时候,经常会发现Firefox 被初始化,提示收藏夹等,遮住元素窗口。因为Firefox每次都以默认的Profile 加载全新的Firefox,例如如下图片:
Snap1.jpg在这样的情况下,我们需要专门准备一份独立的Firefox Profile进行使用。步骤如下
-
在命令行中进入Firefox的安装目录,然后输入
Snap2.jpgfirefox -ProfileManager -no-remote
-
然后在弹出的窗口中,新建一个Profile,记录地址。
e5792a15-e7ed-39fa-bf46-5505c54923a5.png -
根据刚刚的地址,打开Profile所在的文件夹:
Snap3.jpgC:\Users\Linty\AppData\Roaming\Mozilla\Firefox\Profiles
-
复制
Snap4.jpgeiny9uds.selenium
文件夹到base
目录中。 -
修改代码
automate_driver.py
def __init__(self, by_char=",", firefox_profile=None): """ 构造方法:实例化 BoxDriver 时候使用 :param by_char: 分隔符 :param firefox_profile: 可选择的参数,如果不传递,就是None 如果传递一个 profile,就会按照预先的设定启动火狐 去掉遮挡元素的提示框等 """ if firefox_profile is not None: firefox_profile = FirefoxProfile(firefox_profile) driver = webdriver.Firefox(firefox_profile=firefox_profile) try: self.base_driver = driver self.by_char = by_char except Exception: raise NameError("Firefox Not Found!")
-
修改代码
xxx_test_cases.py
profile = "base\\einy9uds.selenium" self.base_driver = AutomateDriver(firefox_profile=profile)
-
完毕。