做UI层自动化比较棘手的问题就是稳定性,强壮性不足的代码运行起来会抛出各种异常让人无奈,在排查问题过程中debug模式的调试加日志信息是可以快速命中报错点的有效方式之一。初学者学习Selenium开始都是为了熟悉Selenium的方法使用,从实例化WebDriver、get方法打开浏览器输入url、findElement方法查找元素、click或sendKeys等方法的操作、最后quit方法关闭浏览器;这是初学者开始学习的一个简单思路,代码也是一个类中从头写到尾;过渡到熟悉后,会想到代码分层封装,Selenium也提供了POM的分层概念。然后加上Log日志、结合单元测试框架实现用例层、并发,最后实现持续集成(Jenkins、Dokcer、Report、EmailToTester)等等。
回到主题EventFiringWebDriver,直译过来就是监听WebDriver的事件,简单举例一下如get()、findElement()、click()这些都是输入WebDriver的事件,那么监听这些事件有什么帮助,下面上代码。
public class LoginPage extends BasePage{
private static Logger logger = Logger.getLogger(LoginPage.class);
public By usernameTextBox = By.id("username");
public By passwordTextBox = By.id("userpwd");
public By loginButton = By.name("button");
public By error_Warn = By.cssSelector(".content");
/**
* 登录失败时的提示语
* @return
*/
public String error_Warn(){
String errorWarn = findElementGetText(error_Warn);
logger.info("得到登录失败提示语:"+errorWarn);
return errorWarn;
}
/**
* 登录操作
* @param list 入参参数
*/
public void login(Listlist){
findElementSendKeys(usernameTextBox, list.get(0));
logger.info("登录用户名输入:"+list.get(0));
findElementSendKeys(passwordTextBox,list.get(1));
logger.info("登录密码输入:"+list.get(1));
findElementClick(loginButton);
logger.info("点击登录按钮");
}
}
如上面的一个简单的登录类,加上了很多的日志来记录操作的步骤,这样出问题时,可以通过日志的打印就可以快速找到报错的位置的代码。缺点也很明显,随着页面上操作的元素增加代码增多,这样的log日志加到想吐。接下来看看EventFiringWebDriver的神奇。
1.实现WebDriverEventListener接口
EventFiringWebDriver的原理后面从源码来分析说明,先简单来个例子熟悉下它的使用;先自定义一个类并实现WebDriverEventListener接口的全部方法。
public class EventListenerLog implements WebDriverEventListener {
@Override
public void afterAlertAccept(WebDriver arg0) {}
@Override
public void beforeAlertAccept(WebDriver arg0) {}@Override
public void afterAlertDismiss(WebDriver arg0) {}
@Override
public void beforeAlertDismiss(WebDriver arg0) {}@Override
public void afterChangeValueOf(WebElement arg0, WebDriver arg1, CharSequence[] arg2) {}
@Override
public void beforeChangeValueOf(WebElement arg0, WebDriver arg1, CharSequence[] arg2) {}@Override
public void afterClickOn(WebElement arg0, WebDriver arg1) {}@Override
public void beforeClickOn(WebElement arg0, WebDriver arg1) {}@Override
public void afterFindBy(By arg0, WebElement arg1, WebDriver arg2) {}
@Override
public void beforeFindBy(By arg0, WebElement arg1, WebDriver arg2) {}@Override
public void afterNavigateBack(WebDriver arg0) {}
@Override
public void beforeNavigateBack(WebDriver arg0) {}@Override
public void afterNavigateForward(WebDriver arg0) {}
@Override
public void beforeNavigateForward(WebDriver arg0) {}@Override
public void afterNavigateRefresh(WebDriver arg0) {}
@Override
public void beforeNavigateRefresh(WebDriver arg0) {}@Override
public void afterNavigateTo(String arg0, WebDriver arg1) {}
@Override
public void beforeNavigateTo(String arg0, WebDriver arg1) {}@Override
public void afterScript(String arg0, WebDriver arg1) {}@Override
public void beforeScript(String arg0, WebDriver arg1) {}
@Override
public void afterSwitchToWindow(String windowName, WebDriver driver){}
@Override
public void beforeSwitchToWindow(String windowName, WebDriver driver){}
@Override
publicvoid afterGetScreenshotAs(OutputType arg0, X arg1) {} @Override
publicvoid beforeGetScreenshotAs(OutputType arg0) {}
@Override
public void onException(Throwable arg0, WebDriver arg1) {}
}
(1)beforeAlertAccept/afterAlertAccept:操作Alert窗口确认前/确认后执行
(2)beforeAlertDismiss/afterAlertDismiss:操作Alert窗口取消前/取消后执行
(3)beforeChangeValueOf/afterChangeValueOf:输入值前/输入值后执行
(4)beforeClickOn/afterClickOn:点击前/点击后执行
(5)beforeFindBy/afterFindBy:使用findElement(findElements)方法前/findElement(findElements)方法后执行
(6)beforeNavigateBack/afterNavigateBack:使用back方法操作浏览器返回前/返回后执行
(7)beforeNavigateForward/afterNavigateForward:使用forward方法操作浏览器前进前/前进后执行
(8)beforeNavigateRefresh/afterNavigateRefresh:使用refresh方法操作浏览器刷新前/刷新后执行
(9)afterNavigateTo/beforeNavigateTo:使用get、to方法在浏览器上输入url访问前/访问后执行(这里提一点,用的最多的get方法,源码中就是使用了to方法的一个封装方法而已,所以是一样的)
(10)beforeScript/afterScript:使用JavaScript前/后执行
(11)beforeSwitchToWindow/afterSwitchToWindow:使用switchTo切换句柄前/后执行
(12)beforeGetScreenshotAs/afterGetScreenshotAs:使用getScreenshotAs方法截图前/截图后执行
(13)onException:使用WebDriver过程中报错时执行
2.EventFiringWebDriver使用
public class Test {
public static void main(String args[]){
WebDriver driver = new EventFiringWebDriver(new ChromeDriver()).register(new EventListenerLog());
driver.get("http://www.baidu.com");
driver.findElement(By.id("kw")).sendKeys("软件测试");
}
}
(1)先初始化EventFiringWebDriver实例,入参的是一个ChromeDriver实例,然后调用register方法,入参的是上面实现了WebDriverEventListener接口的自定义类实例。
@Override
public void afterChangeValueOf(WebElement arg0, WebDriver arg1, CharSequence[] arg2) {
// TODO Auto-generated method stub
System.out.println("输入内容后");
}
@Override
public void beforeChangeValueOf(WebElement arg0, WebDriver arg1, CharSequence[] arg2) {
// TODO Auto-generated method stub
System.out.println("输入内容前");
}
(2)在EventListenerLog的2个方法中加一些打印信息,验证一下结果
(3)执行结果肯定是会执行beforeChangeValueOf/afterChangeValueOf方法,并打印出结果
当然说到EventFiringWebDriver并不是说一定要用来作为日志信息的记录,它的特点就是可以在WebDriver事件发生前后做一些事情,抓住这个特点就可以完成很多东西,比如产生报错截图,就可以在onException中完成。
3.EventFiringWebDriver源码分析
(1)EventFiringWebDriver构造函数
public class EventFiringWebDriver implements WebDriver, JavascriptExecutor, TakesScreenshot,
WrapsDriver, HasInputDevices, HasTouchScreen,
Interactive, HasCapabilities {private final WebDriver driver;
public EventFiringWebDriver(final WebDriver driver) {
Class>[] allInterfaces = extractInterfaces(driver);this.driver = (WebDriver) Proxy.newProxyInstance(
WebDriverEventListener.class.getClassLoader(),
allInterfaces,
(proxy, method, args) -> {
if ("getWrappedDriver".equals(method.getName())) {
return driver;
}try {
return method.invoke(driver, args);
} catch (InvocationTargetException e) {
dispatcher.onException(e.getTargetException(), driver);
throw e.getTargetException();
}
}
);
}
}
从源码可以知道EventFiringWebDriver实现了非常多的接口,熟悉的WebDriver,JavascriptExecutor,TakesScreenshot接口都在这里出现了。实例化EventFiringWebDriver对象的时候执行构造函数的代码就是通过动态代理得到一个代理对象。
(2)EventFiringWebDriver的register方法
private final List
eventListeners = new ArrayList<>();
private final WebDriverEventListener dispatcher = (WebDriverEventListener) Proxy
.newProxyInstance(
WebDriverEventListener.class.getClassLoader(),
new Class[] {WebDriverEventListener.class},
(proxy, method, args) -> {
try {
for (WebDriverEventListener eventListener : eventListeners) {
method.invoke(eventListener, args);
}
return null;
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
);public EventFiringWebDriver register(WebDriverEventListener eventListener) {
eventListeners.add(eventListener);
return this;
}public EventFiringWebDriver unregister(WebDriverEventListener eventListener) {
eventListeners.remove(eventListener);
return this;
}
EventFiringWebDriver的register注册方法,把入参的对象加入到eventListeners这个list中,如果要移除这个监听类可以调用unregister方法
(3)调用get方法
初始化EventFiringWebDriver对象后,开始调用它的get方法
private final WebDriverEventListener dispatcher = (WebDriverEventListener) Proxy
.newProxyInstance(
WebDriverEventListener.class.getClassLoader(),
new Class[] {WebDriverEventListener.class},
(proxy, method, args) -> {
try {
for (WebDriverEventListener eventListener : eventListeners) {
method.invoke(eventListener, args);
}
return null;
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
);
@Override
public void get(String url) {
dispatcher.beforeNavigateTo(url, driver);
driver.get(url);
dispatcher.afterNavigateTo(url, driver);
}
因为EventFiringWebDriver实现了WebDriver接口,所以也实现了它的方法,这里只是举例列出get方法。
dispatcher.beforeNavigateTo(url, driver);
先看看第一句:dispatcher对象也是通过动态代理得到的,在调用register方法入参的对象是自定义的EventListenerLog,然后加入到了eventListeners这个list中。调用beforeNavigateTo方法,则根据dispatcher的动态代理对象通过method.invoke反射调用了EventListenerLog的beforeNavigateTo方法。
driver.get(url);
然后再看第二句:driver也是通过动态代理得到的,入参的是ChromeDriver(ChromeDriver继承RemoteWebDriver,而RemoteWebDriver实现了WebDriver);调用get方法,同样是通过method.invoke反射调用了RemoteWebDriver的get方法。
通过源码来看就很清晰的知道为什么EventFiringWebDriver能实现操作方法前后做一些事情了,就不做什么总结了。