熟悉PageObject模式的朋友一定对 FindBy,FindBys,FindAll 这三个annotation不陌生,借助这三个注解WebDriver提供了一种比较直观的元素管理的解决方案.
但是如果单一页面的元素比较多的时候,都写在一个类里反而不太好管理,这里我们可能会采用两种方式来解决
1 把页面再次分解
2 把元素的xpath等信息写在文件里.
页面分解很好理解,把一个复杂的页面分成若干个类来管理.但是把元素的信息写到文件里又会碰到一个问题,文件的读写.这里我们就可以用自定义annotation来解决。
首先我们先来了解一下,WebDriver是如何通过一个 @FindBy 来组合我们的元素的。提示:以下内容包含大量Java反射内容,最好有所了解.
假如 你有这么一个类
public class BaiduHomePage {
public BaiduHomePage(WebDriver driver) {
PageFactory.initElements(driver, this);
}
@FindBy(id = "kw")
private WebElement inputSearch;
@FindBy(id = "su")
private WebElement buttonSearch;
public void searchByKeyWords(String keyWord) {
inputSearch.clear();
inputSearch.sendKeys(keyWord);
buttonSearch.click();
}
}
那么一切要从
PageFactory.initElements(driver, this);
这一行开始说起
查看源码可以得知
public static void initElements(WebDriver driver, Object page) {
final WebDriver driverRef = driver;
initElements(new DefaultElementLocatorFactory(driverRef), page);
}
注意这里的
DefaultElementLocatorFactory
从名字看就可以知道这是一个元素定位工厂类,具体实现暂且不谈继续往下看
又调用了另一个方法
public static void initElements(ElementLocatorFactory factory, Object page) {
final ElementLocatorFactory factoryRef = factory;
initElements(new DefaultFieldDecorator(factoryRef), page);
}
这里又出现一个
DefaultFieldDecorator
这个类里有两个主要的方法
public Object decorate(ClassLoader loader, Field field)
此方法主要是通过Field以及 ElementLocatorFactory 来返回WebElement,也就是说通过这个方法把源头的
DefaultElementLocatorFactory 和
@FindBy(id = "kw") 修饰的域结合起来 生成一个合法的WebElement
这里面调用了两个 方法
protected WebElement proxyForLocator(ClassLoader loader, ElementLocator locator) {
InvocationHandler handler = new LocatingElementHandler(locator);
WebElement proxy;
proxy = (WebElement) Proxy.newProxyInstance(
loader, new Class[]{WebElement.class, WrapsElement.class, Locatable.class}, handler);
return proxy;
}
这个方法用Java 动态代理的模式把一个不确定的WebElement和通过ElementLocator所定位到的WebElement捆绑在一起.
具体实现会在下一篇文章里讲到.
下面的方法非常类似 只是WebElement变成了List<WebElement>
@SuppressWarnings("unchecked")
protected List<WebElement> proxyForListLocator(ClassLoader loader, ElementLocator locator) {
InvocationHandler handler = new LocatingElementListHandler(locator);
List<WebElement> proxy;
proxy = (List<WebElement>) Proxy.newProxyInstance(
loader, new Class[]{List.class}, handler);
return proxy;
}
protected boolean isDecoratableList(Field field)
此方法主要是通过反射来确定一个域(Field)是否是一个合法的 List<WebElement>
那么回过头来看
DefaultElementLocatorFactory
这里面是怎么从一个 Field 生成 一个可用的Element
首先 DefaultElementLocatorFactory调用
public ElementLocator createLocator(Field field)
生成一个new DefaultElementLocator(searchContext, field) //此处的searchContext就是你的driver
然后在DefaultElementLocator初始化的时候会把Field传递给另外一个类 Annotations
public DefaultElementLocator(SearchContext searchContext, Field field) {
this(searchContext, new Annotations(field));
}
再来看最终的构造方法
public DefaultElementLocator(SearchContext searchContext, AbstractAnnotations annotations) {
this.searchContext = searchContext;
this.shouldCache = annotations.isLookupCached();
this.by = annotations.buildBy();
}
在这里是不是看到了比较熟悉的东西 By
那么在annotations.buildBy()里又发生了什么
public By buildBy() {
assertValidAnnotations(); //校验是否为合法的annotation
By ans = null; //最终要返回的By
FindBys findBys = field.getAnnotation(FindBys.class);
if (findBys != null) { //是否annotation是FindBys
ans = buildByFromFindBys(findBys);
}
FindAll findAll = field.getAnnotation(FindAll.class);
if (ans == null && findAll != null) {//如果By没被初始化并且annotation是FindAll
ans = buildBysFromFindByOneOf(findAll);
}
FindBy findBy = field.getAnnotation(FindBy.class);
if (ans == null && findBy != null) {//如果By没被初始化并且annotation是FindBy
ans = buildByFromFindBy(findBy);
}
if (ans == null) {
ans = buildByFromDefault();//这里也很清晰,就是你可能写过这样的@FindBy("su"),因为FindBy默认是ById和ByName所以也就不用写成了@FindBy(id="su")
}
if (ans == null) {
throw new IllegalArgumentException("Cannot determine how to locate element " + field);
}
return ans;
}
那么通过Annotations所获得的By,
DefaultElementLocator 就可以通过findElement()方法找到WebElement
然后在 DefaultFieldDecorator里通过
protected WebElement proxyForLocator(ClassLoader loader, ElementLocator locator) {
InvocationHandler handler = new LocatingElementHandler(locator);
WebElement proxy;
proxy = (WebElement) Proxy.newProxyInstance(
loader, new Class[]{WebElement.class, WrapsElement.class, Locatable.class}, handler);
return proxy;
}
把WebElement返回给你自己定义的WebElement例如上文中的
private WebElement inputSearch;
因为在LocatingElementHandler 里的invoke方法会首先调用findElement方法确定是否元素存在然后返回。
到这里 一个完整的从@FindBy到最终的WebElement的流程大概清晰了
initElements(WebDriver driver, Object page)->initElements(new DefaultElementLocatorFactory(driver), page)
->initElements(new DefaultFieldDecorator(DefaultElementLocatorFactory), page)->DefaultFieldDecorator.decorate(ClassLoader loader, Field field)
->DefaultFieldDecorator.proxyForLocator(ClassLoader loader, ElementLocator locator)->DefaultElementLocator(SearchContext searchContext, AbstractAnnotations annotations)
->DefaultElementLocator.findElement()->Annotations.buidBy()
那么如果我们想自定义一个Annotation的话就需要做这么几步
1 自定义一个Annotation
2 自定义一个annotation和元素管理文件的中间类
3 自定义一个类继承自Annotations 并且重写buildBy()方法
4 自定义一个ElementLocatorFactory兵重写createLocator()方法,在这里我们会用到DefaultElementLocator和自定义的Annotation
5 自定义一个FieldDecorator,并重写isDecoratableList,因为Annotation已经改变如果再用默认的判断方法那么List<WebElement>查找会出现问题
一个小例子:https://pan.baidu.com/s/1skSP6Ex
有兴趣的朋友可以下载下来参考一下,你可能会用到的jar包有:
selenium-server-standalone-3.0.1.jar
testng-6.8.7.jar
commons-lang3-3.1.jar
json-20160810.jar
代码里使用了JSON 文件来管理元素
注意:实例代码所使用的JDK为1.8并且Selenium版本为3.0.1