由于Selenium框架采用PageObject设计模式,因而提供提供了不少很方便的注解来达到目的,其中有一个注解就是@FindBy。在使用中,只要通过在field中使用注解,则可以将不同属性的元素对象转换成一个WebElement对象。通过WebElement提供的方法,则可以进行UI上的操作了,下面来简单看看这个神奇的注解是怎么工作的。

webElement包含的方法

通过WebElement提供的方法,则可以进行UI上的操作了,下面来简单看看这个神奇的注解是怎么工作的。 webelement

注解@FindBy的使用

@FindBy(name = "修改密码")
public WebElement changePswTab;

通过指定name属性,可以将changePswTab转换成当前页面的一个WenElement对象

注解定义

注解的定义很简单,直接看源码即可,通过注解定义可以知道,可以通过idnameclassNamexpath等多种方式来锁定当前元素

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.TYPE})
public @interface FindBy {
  How how() default How.UNSET;

  String using() default "";

  String id() default "";

  String name() default "";

  String className() default "";

  String css() default "";

  String tagName() default "";

  String linkText() default "";

  String partialLinkText() default "";

  String xpath() default "";
}

注解行为注入

注解的行为肯定是在使用前注入的,那这个@FindBy具体做了什么呢? 1.将元素构建成By对象

  public By buildBy() {
    assertValidAnnotations();

    By ans = null;
    ...

    FindBy findBy = field.getAnnotation(FindBy.class);
    if (ans == null && findBy != null) {
      ans = buildByFromFindBy(findBy);
    }
    ...

    return ans;
  }

Annotations中,selenium通过反射拿到findby对象,然后将对象构建成By对象

  protected By buildByFromShortFindBy(FindBy findBy) {
    if (!"".equals(findBy.className()))
      return By.className(findBy.className());
    ...

    if (!"".equals(findBy.name()))
      return By.name(findBy.name());
    ...

    // Fall through
    return null;
  }

  public static By name(final String name) {
    if (name == null)
      throw new IllegalArgumentException(
          "Cannot find elements when name text is null.");

    return new ByName(name);
  }

表面上注解执行到这里就完成了,但其实可以发现,在field注解使用时,拿到的是一个webelment对象!并不是现在拿到的by对象?这是怎么一回事呢?其实在这里,selenium使用了动态代理的方式来讲by对象转成webelment对象!

动态代理转换实现

通过回溯buildBy的调用方法,可以回溯到PageObject.initElements(...),但其实这个调用树中,起作用的是这一段

  public Object decorate(ClassLoader loader, Field field) {
    if (!(WebElement.class.isAssignableFrom(field.getType())
          || isDecoratableList(field))) {
      return null;
    }

    ElementLocator locator = factory.createLocator(field);
    if (locator == null) {
      return null;
    }

    if (WebElement.class.isAssignableFrom(field.getType())) {
      return proxyForLocator(loader, locator);
    } else if (List.class.isAssignableFrom(field.getType())) {
      return proxyForListLocator(loader, locator);
    } else {
      return null;
    }
  }

selenium通过factory.createLocator(field)来实现by对象的构建,然后将by转换成webelment则是proxyForLocator(loader, locator)来实现的。

  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;
  }

动态代理中,真正调用的是InvocationHandler的实现对象. 当调用代理对象的接口时, 实际上会 通过InvocationHandler.invkoe将调用转发给实际的对象,即new LocatingElementHandler(locator),所以只需要看看LocatingElementHandlerinvoke方法做了啥就知道了。

  public Object invoke(Object object, Method method, Object[] objects) throws Throwable {
    WebElement element;
    try {
      element = locator.findElement();
    } catch (NoSuchElementException e) {
      if ("toString".equals(method.getName())) {
        return "Proxy element for: " + locator.toString();
      }
      throw e;
    }

    if ("getWrappedElement".equals(method.getName())) {
      return element;
    }

    try {
      return method.invoke(element, objects);
    } catch (InvocationTargetException e) {
      // Unwrap the underlying exception
      throw e.getCause();
    }
  }

很明显,调用了findElement()方法,这样就实现了转换。

为什么要用动态代理

因为在对一个页面进行测试时,涉及到很多元素,但真正执行可能只用到其中几个元素而已,使用动态代理的好处就是,不用在我们设计实现的时候就指定某一个代理类来代理哪一个被代理对象, 而把这种指定延迟到程序运行时由JVM来实现,相当于用例执行,元素调用时才去加载元素,简单来说就是实现了元素延迟加载。