Selenium_pagefactory源码阅读

2021-05-18
halley.fang

selenium 做UI自动化会用到 PO 模式,PageFactory 则是为了支持 POM 而开发出来的,它提供初始化页面元素的方法。

PO设计模式

PO(page object)设计模式是在自动化中已经流行起来的一种易于维护和减少代码的设计模式. 在自动化测试中, PO对象作为一个与页面交互的接口. 测试中需要与页面的UI进行交互时, 便调用PO的方法. 这样做的好处是, 如果页面的UI发生了更改,那么测试用例本身不需要更改, 只需更改PO中的代码即可.

PO设计模式具有以下优点:

  • 测试代码与页面的定位代码(如定位器或者其他的映射)相分离.

  • 该页面提供的方法或元素在一个独立的类中, 而不是将这些方法或元素分散在整个测试中.

PageFactory

PageFactory 使用简介

使用 PageFactory 会用到以下2个注解:

  • @FindBy 定义元素定位方式

  • @CacheLookup 缓存元素

先来看一个使用示例:

/**
页面基类
*/
public abstract class BasePage {

    WebDriver driver;

    ...

    BasePage(WebDriver driver){
            this.driver = driver;
            WebDriverWait wait = new WebDriverWait(driver,5,1);
            if(!StringUtils.isEmpty(this.getWaitElementXpath())) {
                if (WaitUntil.waitUntil(wait,this.getWaitElementXpath())) {
                    PageFactory.initElements(driver, this);
                } else {
                    try {
                        throw new Exception("页面初始化失败");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
  }

/**
首页
*/
  public class HomePage extends BasePage {

      //用于显示等待判断页面元素加载,页面加载完成后进行PageFactory初始化
      @Override
      public String getWaitElementXpath() {
          return "//iframe[@src='/assets/home.html']";
      }

      //url
      private String url = "/home";

      //元素
      //首页iframe元素
      @CacheLookup
      @FindBy(xpath = "//iframe[@src='/assets/home.html']")
      private WebElement iframe;

      //逻辑代码
      public Boolean isIframeDisplay(){
          return iframe.isDisplayed();
      };

      //PageFactory初始化
      public HomePage(WebDriver driver){
          super(driver);
      }

  }

上面示例中 HomePage 类 new 一个实例时,WaitUntil 工具类显示等待xpath定位元素,元素加载成功则进行
PageFactory 初始化,初始化后会根据 @FindBy 定位 iframe 元素并赋值,并且将WebElement进行缓存(缓存的是页面元素的引用,如果页面元素发生的变化导致定位方式找不到元素了,元素操作时会报错找不到对应元素),
此时调用实例的 isIframeDisplay() 方法将返回 true,并且在多次调用时 iframe 元素将从缓存中进行读取,不会多次 driver.findBy,
如果没有加 @CacheLookup 注解,则每次调用都会重新 driver.findBy

使用的方法基本上如上所述,那么具体的实现原理是什么呢?下面来阅读一下源码。

PageFactory 源码阅读

从初始化page作为阅读入口:

public static void initElements(WebDriver driver, Object page) {
  final WebDriver driverRef = driver;
  initElements(new DefaultElementLocatorFactory(driverRef), page);
}

public static void initElements(ElementLocatorFactory factory, Object page) {
  final ElementLocatorFactory factoryRef = factory;
  initElements(new DefaultFieldDecorator(factoryRef), page);
}

public static void initElements(FieldDecorator decorator, Object page) {
  Class<?> proxyIn = page.getClass();
  while (proxyIn != Object.class) {
    proxyFields(decorator, page, proxyIn);
    proxyIn = proxyIn.getSuperclass();
  }
}

依次看一下三个initElements重载方法传参都分别做了什么,先来看 DefaultElementLocatorFactory,它主要就是实现了 ElementLocatorFactory 接口,大概的意思就是每次调用都返回新的 ElementLocator

/**
 * A factory for producing {@link ElementLocator}s. It is expected that a new ElementLocator will be
 * returned per call.
 */
public interface ElementLocatorFactory {
  /**
   * When a field on a class needs to be decorated with an {@link ElementLocator} this method will
   * be called.
   *
   * @param field the field
   * @return An ElementLocator object.
   */
  ElementLocator createLocator(Field field);
}

接着看 DefaultFieldDecorator

/**
 * Default decorator for use with PageFactory. Will decorate 1) all of the WebElement fields and 2)
 * List&lt;WebElement&gt; fields that have {@literal @FindBy}, {@literal @FindBys}, or
 * {@literal @FindAll} annotation with a proxy that locates the elements using the passed in
 * ElementLocatorFactory.
 */
public class DefaultFieldDecorator implements FieldDecorator {

  protected ElementLocatorFactory factory;

  public DefaultFieldDecorator(ElementLocatorFactory factory) {
    this.factory = factory;
  }

  public Object decorate(ClassLoader loader, Field field) {
    //不是WebElement类型的属性或者List<WebElemtnt>的则返回null
    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;
    }
  }

  //主要是针对List<WebElemtnt>进行的判断
  protected boolean isDecoratableList(Field field) {
    if (!List.class.isAssignableFrom(field.getType())) {
      return false;
    }

    // Type erasure in Java isn't complete. Attempt to discover the generic
    // type of the list.
    Type genericType = field.getGenericType();
    if (!(genericType instanceof ParameterizedType)) {
      return false;
    }

    Type listType = ((ParameterizedType) genericType).getActualTypeArguments()[0];

    if (!WebElement.class.equals(listType)) {
      return false;
    }

    if (field.getAnnotation(FindBy.class) == null &&
        field.getAnnotation(FindBys.class) == null &&
        field.getAnnotation(FindAll.class) == null) {
      return false;
    }

    return true;
  }

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

  //List<WebElemtnt>动态代理
  @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;
  }

}

看一下代理实现

public class LocatingElementHandler implements InvocationHandler {
  private final ElementLocator locator;

  public LocatingElementHandler(ElementLocator locator) {
    this.locator = locator;
  }

  public Object invoke(Object object, Method method, Object[] objects) throws Throwable {
    WebElement element;
    try {
      //实际就是调用了driver.findElement(by)
      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();
    }
  }
}

DefaultElementLocator

public WebElement findElement() {
  //缓存中有则读缓存
  if (cachedElement != null && shouldCache()) {
    return cachedElement;
  }
  //driver.findElement(by)
  WebElement element = searchContext.findElement(by);
  //如果有@CacheLookup注解则进行缓存
  if (shouldCache()) {
    cachedElement = element;
  }

最后再看一下 proxyFields

private static void proxyFields(FieldDecorator decorator, Object page, Class<?> proxyIn) {
  //反射获得所有的属性
  Field[] fields = proxyIn.getDeclaredFields();
  //遍历属性
  for (Field field : fields) {
    //调用装饰器对属性进行装饰,即上面看到的DefaultFieldDecorator
    Object value = decorator.decorate(page.getClass().getClassLoader(), field);
    if (value != null) {
      try {
        //设置反射的属性可以访问
        field.setAccessible(true);
        //属性设置装饰后的新值
        field.set(page, value);
      } catch (IllegalAccessException e) {
        throw new RuntimeException(e);
      }
    }
  }
}

好了,到此为止 PageFactory 的原理基本上清晰了,还有一个 @FindBy 传值定位元素方式的源代码比较直白易懂,而且由于本人接触的项目都是属于平台类型的,基本不会使用到 id name 这些方式,所以本文中就暂不赘述了,感兴趣的同学自己打开源码看一看吧。

@CacheLookup 最好谨慎使用,一般是确定且已成功加载的元素才使用进行缓存;如果元素加载缓慢一般我们会使用显示等待的方式,如果超时了会抛出异常,如果缓存了则会等到具体操作元素时才会抛出异常


Similar Posts

Comments

Table of contents