引子
2011年初来公司实习的时候,接的第一份活就是维护UI自动化用例,从此开始我轰轰烈烈的Tester生涯,此处省略十万字。。。
经历第一代UI自动化的没落,DWR接口测试的兴起,以及直接参与项目组的功能测试,最终又回到了一年前的原点。
思考良多,苦逼地推出了第二代UI自动化框架,大名Dagger。
废话少说,先讲技术选型
由于历史传统,Selenium2.0成为不二选择,Selenium2.0是Selenium1.0与WebDriver的合体,新框架Dagger是以WebDriver为基础。
再讲设计思想和定位
新框架之所以取名Dagger(匕首),就是希望它和匕首一样轻便灵巧,框架专注于实现各种Web操作的封装。其他的外围,如数据库连接,分布式执行,持续集成等则根据情况添加,将开发,维护,使用,分析用例的成本尽可能降低。
打个比方,匕首绑在木棍上就是长矛,装在步枪上就是刺刀,可以随机应变。因此,Dagger是如此之小,小到核心类只有3个,架包2个。
框架小巧方便,则可以把主要精力放在用例的实现和组织上,使用例尽量贴近业务。
怎样写用例,怎样封装xpath,怎样组织用例,才可以使写用例和维护用例成本最低?不仅仅是技术问题,更多的是实践经验的积累。
抛砖引玉,关于UI自动化的一些设想
一次性用例,主要用于验证功能是否正常上线;
SeleniumIDE,是Selenium自带的自动化录制工具,支持将录制用例直接输出为JAVA代码。充分掌握这一工具后,可以大幅提高写用例的效率;
一个用例就是一个类,内含所须的元素定位Xpath和各种驱动数据;
优势:不同的人同时写用例不会互相干扰,责任清晰明确。
缺点:潜在的维护成本。
责任人制度(很多时候用例的成效上不去,是因为用例挂了以后没有责任人及时分析,反馈维护);
核心的,稳定的功能,用例写的规范些,便于阅读和维护;
普通的,易变的功能,用例写的随便些,挂了就重写一个;
UI自动化用于回归,要像脚本,不要像代码,就是手动回归的脚本化;
用例傻瓜化,容易写容易读容易维护;
一个用例一组专用账号,方便隔离和数据准备,也便于后续的用例分布式并行执行;
最关键的一点,自动化必须贴合业务!
下面上代码(节约版面,部分节选)
架包只有2个:
selenium-server-standalone-2.18.0.jar
testng-6.3.1.jar
框架核心类3个:
BrowserEmulator 浏览器类,一个浏览器的抽象
GlobalSettings 所有的配置项都统一放在这里
PageElement 页面元素类
先讲BrowserEmulator
宾老板曾问我,为啥要把Selenium再封装一层,变成BrowserEmulator这个类?宾老板永远是那么的犀利,吓得我颇心虚。幸好我是有备而来,理由有三:
1. Selenium本身提供了大量API,这是好事吗?我觉得不是,功能强大是好,但里面95%的API都不是常用的,徒增烦恼罢了,因此加一层封装;
2. Selenium2.0(Webdriver)有不少Bug和缺陷,谁用谁知道!因此要加一层封装,补全之。
3. 加了一层封装以后,哪怕Selenium大变样甚至以后不再用Selenium了,也可以保证BrowserEmulator对外提供接口的稳定性。
代码如下,穿插说明。
/**
* Selenium2.0二次封装
* @author 尘泥
*/
public class BrowserEmulator {
WebDriver BrowserCore;
WebDriverBackedSelenium Browser;
这里有一个三层体系,最里面的是WebDriver,外面包一层WebDriverBackedSelenium,最后再包一层用户直接接触的BrowserEmulator。看官可以在下文看到有些API是基于WebDriver实现的,有些则是基于WebDriverBackedSelenium,看起来很囧,不是么?理论上,所有的Web操作只用WebDriver相关的API就可以实现,但是,亲,要自己写代码实现哦!很麻烦,考虑到这一点,Selenium2.0本身就提供WebDriverBackedSelenium类,把WebDriver类包装一层,并对外提供大量类似于Selenium1.0的API,用起来很爽,是不是?不是,因为WebDriverBackedSelenium模拟了Selenium1.0的API,但模拟比较蹩脚,有些API有Bug,有些则根本没实现。所以,还得回过来靠WebDriver实现。一会要用WebDriverBackedSelenium,一会要用WebDriver,可以想见,如果不在最外面封装一层BrowserEmulator,用户使用起来会是何等的苦逼!?当然了,熟练的自动化测试工程师不在此例。
ChromeDriverService ChromeServer;
要使用Chrome浏览器的话,还必须起一个server。不过即便要多起一个server,chromedriver还是比firefoxdriver启动快得多!
JavascriptExecutor js;
加个JS执行器,有些东西还得靠JS直接来实现,详见下文。
public BrowserEmulator() {
chooseBrowserCoreType(GlobalSettings.BrowserCoreType);
根据配置参数选择浏览器类型,目前只支持FFdriver和Chromedriver,理论上,还可以支持IEdriver和htmlUnitdriver,但实际上,虽然它们都是实现了webdriver接口,但是实现的具体代码各不相同,导致一些Web操作在不同driver之间有兼容性问题。具体问题,下文将有涉及。
Browser = new WebDriverBackedSelenium(BrowserCore, "www.163.com");
Browser.setSpeed(GlobalSettings.StepInterval);
// TODO 这里Selenium2.0存在Bug,setSpeed()实际上无法设置运行速度。
Browser.setTimeout(GlobalSettings.Timeout);
js = (JavascriptExecutor) BrowserCore;
}
…
…
/**
* 在iframe中输入文本
* @param locator Xpath Of Frame
* @param Text
*/
这个方法目前只支持FF,chrome下不行,解决中。。。
public void typeInFrame(String locator, String Text) {
pause();
waitForElementPresent(locator);
// 进入指定iframe
WebElement myframe = BrowserCore.findElement(By.xpath(locator));
BrowserCore.switchTo().frame(myframe);
// 进入编辑节点
WebElement editable = BrowserCore.switchTo().activeElement();
editable.sendKeys(Text);
// 返回
BrowserCore.switchTo().defaultContent();
}
/**
* Robot敲击键盘
* @param KeyCode
*/
出于安全性考虑,有些Web操作要求监听“真正的”键盘事件,这种情况应该也是比较常见的。所以这里使用java原生提供的Robot实现模拟键盘事件。而且键盘可以帮我们解决很多问题,比如,我按一下F5就可以实现页面刷新,那就不须要额外再为刷新页面写一个API了,又可以使代码简洁一些了。很多时候不能依赖框架提供这样那样几乎万能的API,其实自己完全可以想办法绕过去的。
public void pressKeyboard(int KeyCode) {
pause();
Robot rb = null;
try {
rb = new Robot();
} catch (AWTException e) {
e.printStackTrace();
}
rb.keyPress(KeyCode); // 按下按键
rb.delay(100); // 保持100毫秒
rb.keyRelease(KeyCode); // 释放按键
}
/**
* Robot控制鼠标
* @param positionX
* @param positionY
* @param KeyCode
*/
原理同上一个API
这个API主要是为Flash自动化准备的。处于安全性的考虑,如果Flash操作将导致页面离开这个Flash or 会有表单提交之类的,那么Flash安全框架会强制要求这一次click必须是“真实的”鼠标点击。
public void pressMouse(int positionX, int positionY, int KeyCode) {
}
/**
* 模拟Hover动作
* @param locator
* @note 暂时只支持ChromeDriver
*/
Selenium2.0本身就是原生提供mouseOver这个函数的。但是,至少在2.18.0,这个API还是一个废材。。。
public void mouseOver(String locator) {
pause();
waitForElementPresent(locator);
以下这段代码似乎不是很灵光,优化进行中。。。
// Hover
WebElement we = BrowserCore.findElement(By.xpath(locator));
Actions builder = new Actions(BrowserCore);
builder.moveToElement(we).build().perform();
return;
}
/**
* 在Flash中点击元素
* @param flashID flash本身在网页中ID
* @param targetName 目标元素在flash中Name
*/
关于Flash自动化,我将另开一文讲解(传送门:http://qa.blog.163.com/blog/static/19014700220121278928634/),里面会用到JS执行器。
public void flexClick(String flashID, String targetName) {
}
/**
* 获取Flash中元素左上角的(浏览器,非屏幕)坐标
* @param flashID flash本身在网页中ID
* @param targetName 目标元素在flash中Name
* @return 坐标[x,y]
*/
public int[] flexGetPosition(String flashID, String targetName) {
}
/**
* 设置BrowserEmulator各次操作之间的时间间隔,单位毫秒
*/
Selenium2.0原生提供setSpeed函数,用以控制用例执行速度,但是,至少至2.18.0,这个API还是个废材。。。只好自己实现一个。
基本上调试的时候会把步长设为500毫秒,真正运行时设为0毫秒。
private void pause() {
此处使用万恶的Thread.sleep
}
从这里开始是几个元素定位和判断的函数,所谓用例自然要有判定,才有成功与失败之分,此处省略N行代码。。。
/**
* 页面元素定位
* @param locator 暂时只支持xpath,后续考虑支持更多形式,如:id,name
*/
private void waitForElementPresent(final String locator) {
int Timeout = Integer.parseInt(GlobalSettings.Timeout);
try {
new Wait() {
public boolean until() {
return Browser.isElementPresent(locator);
}
}.wait("*** 页面元素(" + locator + ")定位失败 ***", Timeout);
} catch (Exception e) {
Assert.fail("*** 页面元素(" + locator + ")定位失败 ***");
// TODO 更多自定义信息打印
}
}
再讲GlobalSettings
不说了,很短小
* 全局变量设置
* @author 尘泥
*/
public class GlobalSettings {
/**
* 设置浏览器类型
* 1 FireFox
* 2 Chrome
**/
public static int BrowserCoreType = 2;
public static String ChromeDriverPath = "res/chromedriver.exe";
// BrowserEmulator各次操作之间的时间间隔,单位毫秒
public static String StepInterval = "500";
// BrowserEmulator等待超时时间,单位毫秒
public static String Timeout = "30000";
}
最后是PageElement
对页面元素做了简单封装,可以使元素定位清晰一些。
* 用于页面元素定位
* @author 尘泥
*/
public class PageElement {
// 页面元素Xpath
private String xpath = null;
/**
* 构造函数
* @param tag 节点tag
* @param attr 节点属性,如:id name class
* @param value 属性值
*/
public PageElement(String tag, String attr, String value) {
this.xpath = "//" + tag + "[@" + attr + "='" + value + "']";
}
// 私有构造函数
private PageElement(String xpath) {
this.xpath = xpath;
}
/**
* 加入子节点
* @param tag 子节点tag
* @param value 子节点属性
* @param attr 属性值
* @return 子节点
*/
public PageElement nextNode(String tag, String value, String attr) {
return new PageElement(this.xpath + "/" + tag + "[@" + attr + "='" + value + "']");
}
此处省略N个nextNode函数的不同实现。
}
终章
除了框架核心代码以外,还提供了七八个demo用例,演示了一些写用例的惯用法。正如云风所说,学习语言就是学习它的惯用法,写用例也是一样。API仔仔细细看几眼就知道,怎么写出优质的用例来,才考验功夫。