使用Gradle打包jar

在build.gradle中新增任务releaseJar,类型为jar

task releaseJar(type: Jar) {
  //设置manifest.mf的属性
  manifest {
      attributes 'Implementation-Title': 'Gradle Jar File Example',
              'Implementation-Version': version,
              'Main-Class': 'runqq'
  }
  //jar包包名
  baseName = project.name
  //jar包版本
  version=this.version
  //定义Classifier
  classifier="JDK17"
  //jar文件的输出路径,相对位置
  destinationDir=new File("artifacts/")
  //自定义jar包名称,如不自定义会为baseName+version+classifier.jar
  //archiveName="demo.jar"

  //from对象接受一个文件路径集合,包含要打包到jar中的文件
  from {
      //从工程编译输出的目录中递归包含所有文件及其路径结构
      configurations.compile.collect {
          it.isDirectory() ? it : zipTree(it)
      }
  }
  with jar
}

上述代码是一个task,可供单独执行,当然也可以重写Gradle默认的jar方法来实现打包,将task releaseJar(type: Jar) 改为jar即可。

Web自动化测试中的页面模型(四)-页面模型管理

在Web自动化测试中,如果使用页面模型,页面类一多,就不可避免的会遇到页面模型的管理问题。

如果还需要配合cucumber之类的BDD工具,我们还需要一个页面工厂来管理页面类与实际名称的对应关系。

通常这样的页面类可能会有一个对应关系的列表:

static Map BUILTIN_PAGES = new HashMap(){
    {
        put("Login Page", LoginPage.class)
        put("Home Page", MainPage.class)
        put("System Page", SystemSetting.class)
    }
}

当然也可能是其他形式,这里先以Map形式来说明,配合这个map,可能会有一个方法来创建一个具体的页面:

static Object createPage(pageName){
    return BUILTIN_PAGES.get(pageName).newInstance()
}

那么,问题就来了,随着页面的增多,Map对象会越来越来管理,特别是如果某些特殊的情况下类名或者包名还可能会重复,将来维护起来也会很困难。

这里提供一个使用注解的页面管理方法来代替简单工厂,将来的维护关注点也能直接关注到页面类。

这里有一个简单的工程结构:

pages包里包含了页面相关的内容。我们简单的设计下Page这个注解

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface Page {

    /**
     * page's name
     * @return
     */
    String pageName()

    /**
     * page's type,default is USER_PAGE
     * @return
     */
    PageType pageType() default PageType.USER_PAGE
}

其中,pageName方法用于设置当前类对应的名称,pageType方法用于设置页面的类型,该类型是一个虚构的枚举,我们假设默认的页面为普通用户页面。这个方法可有可无,看实际需要。

有了注解,我们就可以把注解给具体的页面打上了,打上后的页面类可能是这样的

普通人员页面:

@Page(pageName = "user page 1")
class Page1 extends AbstractPage{

    Page1(){
        menuPath="a->b->c"
        tabName="page1 tab"
    }
}

管理员页面:

@Page(pageName = "group admin page",pageType = PageType.GROUP_ADMIN_PAGE)
class GroupAdminPage extends AbstractPage{
    GroupAdminPage(){
        menuPath="d->e"
        tabName=null
    }
}

以上2个页面类我虚构了一个抽象父类AbstractPage,里面定义了2个虚构的在当前系统所有页面都会有的属性:一个是进入页面的菜单路径menuPath,一个是页面可能存在的具体页签名称tabName。同样这两个属性是可有可无的。

现在我们的所有页面都有了自己对应的页面名称,现在我们修改下简单页面工厂,来自动获取指定包下所有带Page注解的类生产对应关系

这里面为了方便用了Reflections,这个包可以很容易在maven库里找到。

class PageFactory {

    //logger
    private final static Logger logger = LogUtils.getLogger(PageFactory)

    //package contains all pages
    final static String PACKAGE_NAME = "pages.production"

    static private Map<String, Class> pages = new HashMap<>()

    /**
     * get all pages
     * @return
     */
    static Map<String, Class> getPages() {
        if (pages.isEmpty())
            init()
        pages
    }

    /**
     * get a page by page name
     * @param pageName name of a page
     * @return
     */
    static Class get(String pageName) {
       def pageClass= getPages().get(pageName)
        if(!pageClass){
            throw new ClassNotFoundException("can not find page [${pageName}]")
        }
        pageClass
    }

    /**
     * get a page instance
     * @param pageName name of a page
     * @return
     */
    static AbstractPage createPage(String pageName) {
        def page = get(pageName).newInstance()
        if (!(page instanceof AbstractPage)) {
            throw new ClassCastException("page name [${pageName}] create a page [${page.class}] is not a instance of AbstractPage.")
        }
        page
    }

    /**
     * init all pages
     */
    private static void init() {
        pages.clear()
        //get all classes of a package by Reflections.
        Reflections reflections = new Reflections(PACKAGE_NAME)
        def classes = reflections.getTypesAnnotatedWith(Page)
        for (def page : classes) {
            def pageName = page.getAnnotation(Page).pageName()
            logger.debug("loading [${pageName}]:[${page}]")
            pages.put(pageName, page)
        }
    }
}

现在我们可以简单的使用

  AbstractPage page = PageFactory.createPage(pageName)

来获得一个页面的实例了。

通过以上的方法,我们还可以定义组件注释来标明组件、初始化的时候根据页面类型进行不同的处理等操作。这里就不在复述了。

示例工程地址:https://github.com/assilzm/PageObjectFactoryDemo

WEB自动化测试中的页面模型(三点五)–树组件设计

上一章中,我们有简单的介绍下一个列表组件该怎么设计,给出了我简单的设计思路。 本来这一章准备写点复杂的页面设计的,但是看平时很多人对树结构很头疼,因此在本章中,我以一个比较完整的树结构的设计设计对上一章进行一点补充。 本次我们要设计一个简单的树,随便找了个例子:http://www.ztree.me/v3/demo.php#102如图:

tree

这棵树具备以下特征:

  1. 树可以有多个根节点
  2. 树需要通过节点前方的+或-开关打开关闭子节点
  3. 展开节点后父节点不会消失
  4. 子节点在父节点的子元素中

树可能会有很多的变种,这里我们仅以符合上面特征的树作为例子,有其他特殊需求的同学请自行扩展。 观察这个树结构,我们发现我们可能需要用到的一些属性,以方便以后如果要支持其他树结构可以进行简单的修改支持,这里我们仅以例子中的结构为例,把这些特征设计为属性,它们如下:

  1. 树的特征,或者说是树所在页面中的容器选择器特征
  2. 根节点的选择器
  3. 父节点展开后子节点所在的容器
  4. 节点在树结构中的元素(如该例子中是一个li下的a元素,只有该元素接受点击事件)
  5. 开关的选择器特征,以及打开、关闭状态的选择器特征

好了,特征大概就是这些,我们根据这些特征,先设计好树控件的属性:

private final static Logger logger = LogUtils.getLogger(Tree)

/**
 * tree container tag name
 */
final static String CONTAINERTAGNAME = 'div'

/**
 * tree container attribute name
 */
final static String CONTAINERATTRIBUTENAME = "class"

/**
 * tree container attribute value
 */
final static String CONTAINERATTRIBUTEVALUE = "zTreeDemoBackground"

/**
 * switcher selector
 */
final static String SWITCHERSELECTOR = "span[starts-with(@class,'ico')]"

/**
 * switcher attribute to verify
 */
final static String SWITCHERATTRIBUTENAME = "class"

/**
 * tree node switcher closed class
 */
final static String SWITCHERCLOSEDATTRIBUTEVALUE = "icoclose"

/**
 * tree node switcher opened class
 */
final static String SWITCHEROPENEDATTRIBUTEVALUE = "icoopen"

/**
 * root node selector
 */
final static String ROOTNODESELECTOR = "ul[@class='ztree']/li[@class='level0']"

/**
 * node element tag name
 */
final static String NODESELECTOR = "a"

/**
 * sub node container selector
 */
final static String SUBNODECONTAINERSELECTOR = "ul/li"

/**
 * container selector
 */
String containerSelector = null

/**
 * current tree container selector with default value
 */
Tree() {
    containerSelector = "//$CONTAINERTAGNAME[@$CONTAINERATTRIBUTENAME='$CONTAINERATTRIBUTEVALUE']"
}

/**
 * current grid container selector with custom value
 * @param selector
 */
Tree(String selector) {
    containerSelector = selector
}

有了这些属性,我们设计一下取得一个节点的选择器的方法,假设我们要找到一个节点,必须知道这个节点所经过的路径,比如:[根节点(父节点1)->父节点2->父节点3->父节点4->父节点5->所需要找的节点],我们把这个节点序列设计成一个List。可能所需要操作的步骤如下:

  1. 找到第一个父节点(也就是根节点)
  2. 如果父节点有子节点(节点左边的开关控件显示+),那么展开它
  3. 找到第二个父节点
  4. 如果父节点有子节点(节点左边的开关控件显示+),那么展开它

….重复1和2…

直到节点序列最后一个元素找到,返回该节点的选择器

我们设计一个getNodeSelector方法,传入一个为List的节点路径参树,生成选择器。这个方法可能用到其他两个方法:根据提供的显示文本返回某个子节点选择器的方法getSubNodeSelector、展开某个节点的方法unfoldNode getNodeSelector:

/**
 * get node selector with node path list
 * @param pathList node path,eg:[rootNodeText,firstNodeText,secondNodeText]
 * @return the selector of node path
 */
String getNodeSelector(List<String> pathList) {
    int pathSize=pathList.size()
    assertTrue("node path can not be empty", pathSize > 0)
    String nodeSelector = containerSelector
    for (int i=0;i< pathSize;i++) {
        if (i!=pathSize-1&&!unfoldNode(nodeSelector))
            return null
        nodeSelector = getSubNodeSelector("$nodeSelector/$SUB_NODE_CONTAINER_SELECTOR", pathList.get(i))
    }
    logger.debug("create node selector:$nodeSelector")
    return "$nodeSelector/$NODE_SELECTOR"
}

getSubNodeSelector:

/**
 * get a sub node selector with displayed text of a node selector
 * @param nodeSelector father node selector
 * @param subNodeText  displayed text of sub node
 * @return sub node selector
 */
private String getSubNodeSelector(String nodeSelector, String subNodeText) {
    List<String> subNodeNames = getTexts(nodeSelector)
    logger.debug("sub node texts:$subNodeNames")
    int nodeIndex = subNodeNames.indexOf(subNodeText)
    if (nodeIndex == -1)
        return null
    return nodeSelector + "[$nodeIndex]"
}

unfoldNode:

/**
 * unfold a father node
 * @param nodeSelector father node selector
 * @return if it is unfolded
 */
boolean unfoldNode(String nodeSelector) {
    boolean hasSubNodes = false
    WebElement switcher = findElement("$nodeSelector/$SWITCHER_SELECTOR")
    if (switcher.getAttribute(SWITCHER_ATTRIBUTE_NAME).contains(SWITCHER_CLOSED_ATTRIBUTE_VALUE)) {
        logger.debug("unfold node:$nodeSelector")
        switcher.click()
        hasSubNodes = true
    }
    return hasSubNodes
}

好了,现在我们可以取得任意的一个节点的选择器了。如果我们要点击一个节点,可以很简单的完成:

/**
 * click at a node
 * @param pathList node path,eg:[rootNodeText,firstNodeText,secondNodeText]
 */
void clickNode(List<String> pathList){
    String nodeSelector=getNodeSelector(pathList)
    assertNotNull("assert node selector of $pathList is exist",nodeSelector)
    click(nodeSelector)
}

接下来只要扩展方法,完全可以实现所有的其他操作。大家自行扩展即可,该代码也可以在示例工程中找到。 PS:细心的同学可以可以发现,我们定义的根节点根本没有用上。其实如果该页面中只有一棵树,我们完全可以不管树所在的容器,直接使用根节点的ztree这个class来完成定位,至于怎么修改一下该控件结构来达到这个目的.同样,有的地方对于异常数据的验证也不完整,就交给大家自己解决啦~ 配合上前面几张所讲的页面控件的调用方法,我们要点击”叶子节点122”,就可以如下操作啦:

Tree tree=new Tree()
String nodePath="父节点1->父节点12->叶子节点122";
List<String> nodeList= Arrays.asList(nodePath.split("->"))
tree.clickNode(nodeList)

补充就到这里,复杂页面的设计我有空了会更新。

下一章:Web自动化测试中的页面模型(四)-页面模型管理