基于Docker集群的分布式测试系统DDT(DockerDistributedTest)

1.背景

当自动化用例累积的越来越多,回归自动化用例的时间越来越长。

我们往往会选择使用多线程的方式来跑用例集,但是用例数量达到一定数量级(千级以上)后,在单台机器上使用多线程(千级以上)直接影响到机器性能,能不能组成并行加并发的模式跑用例,自动将用例集拆分成更细粒度的子集,将子集在单独的容器内(容器可以部署在多台机器上)内并发执行。

1-1.业内工具调研

Selenium grid

参见:https://github.com/SeleniumHQ/selenium/wiki/Grid2

JMeter

参见:https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.pdf
http://jmeter.apache.org/usermanual/remote-test.html

HadoopUnit

参见:http://link.springer.com/chapter/10.1007%2F978-3-642-32122-1_3
http://baidutech.blog.51cto.com/4114344/743834

Selenium grid, JMeter都是在自身的工具内实现分布式测试,通用性较差;HadoopUnit不得不说用大数据的思维来处理分布式测试,在2011年就有这种想法还是很前卫的;由于Docker的便利,现在开发的生产环境已开始大规模使用,而自动化测试领域也必将有这个趋势。

1-2.DockerDistributedTest

最终形成了基于Docker集群的分布式测试系统DDT(DockerDistributedTest);
该系统使用Docker容器作为子集的执行容器,Docker镜像中打包了用例所需运行环境(Java环境)、测试工程及分布式组件TaskTrack,并将测试工程依赖的jar包挂载到Docker容器中。

2.摸索

2-1.用例执行的方式

我们自动化测试工程是maven项目,用例使用TestNG编写;

maven的Surefire测试插件(http://maven.apache.org/surefire/maven-surefire-plugin/)封装了很多执行TestNG和JUnit用例的方法,但是执行mvn命令会输出很多stdout, 同时在新启动的Docker容器内执行mvn命令,会根据pom.xml去download一些依赖的jar,这样就增加了执行用例的前期时间,虽然方法很便捷,但是耗时相对会增加;

故直接使用TestNG命令行,使用TestNG命令需要指定classpath, 我们将实体机中测试工程依赖的jar目录挂载到Docker容器中,TestNG直接依赖classpath, 直接运行TestNG命令跑用例。

获取maven工程依赖的jar包可以通过如下方式获取:

mvn dependency:copy-dependencies -DoutputDirectory=/data1/hugang/docker-distributedtest/docker-addresource/lib_path

启动单个docker容器时,使用-v将测试工程依赖的jar包挂载到容器中:

docker run -d --net=host -v /data1/hugang/docker-distributedtest/docker-addresource/lib_path:/lib_path  -it docker-distributed-self 

每个docker容器使用TestNG命令执行测试用例:

java -classpath '/FastTest/target/test-classes/:/lib_path/*' org.testng.TestNG -parallel methods -testclass com.weibo.qa.testcase.strategy.测试类1,测试类2,...


其中:
/FastTest/target/test-classes/ 为测试工程FastTest的class文件目录;可以通过mvn clean test-compile生成。

/lib_path/* 为测试工程中依赖的jar包。

-parallel methods:方法级并发执行。

org.testng.TestNG使用参数:

Usage: <main class> [options]
 The XML suite files to run
  Options:
    -configfailurepolicy              Configuration failure policy (skip or continue)
    -d                                Output directory
    -dataproviderthreadcount          Number of threads to use when running data providers
    -excludegroups                    Comma-separated list of group names to  exclude
    -groups                           Comma-separated list of group names to be run
    -junit                            JUnit mode (default: false)
    -listener                         List of .class files or list of class names implementing ITestListener or ISuiteListener
    -log, -verbose                    Level of verbosity
    -methods                          Comma separated of test methods (default: [])
    -methodselectors                  List of .class files or list of class names implementing IMethodSelector
    -mixed                            Mixed mode - autodetect the type of current test and run it with appropriate runner (default: false)
    -objectfactory                    List of .class files or list of class names implementing ITestRunnerFactory
    -parallel                         Parallel mode (methods, tests or classes)
    -port                             The port
    -reporter                         Extended configuration for custom report listener
    -suitename                        Default name of test suite, if not specified in suite definition file or source code
    -suitethreadpoolsize              Size of the thread pool to use to run suites (default: 1)
    -testclass                        The list of test classes
    -testjar                          A jar file containing the tests
    -testname                         Default name of test, if not specified in suitedefinition file or source code
    -testnames                        The list of test names to run
    -testrunfactory, -testRunFactory  The factory used to create tests
    -threadcount                      Number of threads to use when running tests in parallel
    -usedefaultlisteners              Whether to use the default listeners (default: true)
    -xmlpathinjar                     The full path to the xml file inside the jar file (only valid if -testjar was specified) (default: testng.xml)

2-2.分布式任务调度工具选型

TestNG

早在2006年2月,TestNG 4.5版本中就新增了Distributed TestNG特性(http://beust.com/weblog2/archives/000362.html),遗憾的是,作者Cedric Beust已经不维护,将Distributed classes移出,独立的放在:https://github.com/testng-team/testng-distributed, 现在的TestNG已不支持Distributed Test。国内有人研究过,有兴趣的可以了解下:http://markshao.github.io/blog/2014/03/01/new-testng/,由于是非官方的,并且也不是很完善,我们没采用他们的方式。

Gearman

入门简单,由worker、client和Job server组成,client发送任务给Job Server,Job Server将任务传送给worker执行并将结果返回给client(http://gearman.org/getting-started/);尝试过,但是不稳定,外网机器调度内网机器或内网机器调度内网机器,会发生无法执行任务的情况,猜测可能是Gearman Protocol(http://gearman.org/protocol/)被公司网络限制。

bistro

facebook开源的一款分布式任务调度工具(c++开发, https://github.com/facebook/bistro), Bistro needs a 64-bit Linux, Folly, FBThrift, boost, and libsqlite3. Caveats: You need about 2GB of RAM to build, as well as GCC 4.8 or above. 安装时,依赖的一些资源,国内无法下载,故没安装上; 项目也没怎么维护。

celery

Python开发的一款分布式任务队列(https://github.com/celery/celery),用的人还是蛮多的,github上5k+个star,因为我们的工程是java编写,最好使用有java api的工具,故没使用该工具。

LTS

Java开发的一款分布式任务调度工具(https://github.com/ltsopensource/light-task-scheduler),提供了完整的文档、使用示例和前端控制台,并且有丰富的java api,可以将调度方有效的结合到测试工程中;故我们使用LTS作为我们分布式测试调度工具; LTS主要由JobClient(负责提交任务,并接受结果)、JobTracker(接收并分配任务)、TaskTracker(执行任务,结果反馈给JobTrack)组成,同时需要配置zookeeper和mysql, 详见github主页LTS用户文档。

3.实现

完整的流程图:

DockerDistributedTest系统由自动化测试工程、用例子集发送器(将测试集自动拆成N个子集)、JobTrack中转站、执行用例Docker集群、测试结果解析器组件构成。

3-1.自动化测试工程

自动化测试工程FastTest, 用例使用TestNG编写,支持http接口测试、rpc服务测试等。

3-2.用例子集发送器

用例子集发送器由JobClient和测试类探测器(https://github.com/neven7/TestClassFinder)组成;

测试类探测器可以根据全限包正则表达匹配出该包下所有的测试类名(JUnit或TestNG测试类):

// 正则匹配
String[] filterPatterns = { "com.weibo.qa.testclassfinder.test.*Test" };
TestClassFinder tcn = new TestClassFinder(filterPatterns);

System.out.println(tcn.find());
System.out.println(tcn.classNameList(tcn.find()));

输出:
[class com.weibo.qa.testclassfinder.test.JUnitTest]
[JUnitTest]

根据测试类探测器得出的测试类集合,拆分成N个子集(N为执行docker实例的数量):

    /**
     * 将testClassName list拆分成TASK_TRACKER_NUM个子list
     * 
     * @param testClassName
     * @return
     * @author hugang
     */
    public static List<String> convertClassStr(
            List<String> testClassNameList) {
        if (testClassNameList == null) {
            throw new NullPointerException("the testClassName list is null .");
        }

        if (TASK_TRACKER_NUM <= 0) {
            throw new IllegalArgumentException(
                    "TASK_TRACKER_NUM must be more than 0");
        }


        List<List<String>> subListResult = new ArrayList<List<String>>(
                TASK_TRACKER_NUM);

        for (int i = 0; i < TASK_TRACKER_NUM; i++) {
            subListResult.add(new ArrayList<String>());
        }

        int index = 0;
        for (String testClassName : testClassNameList) {
            subListResult.get(index).add(testClassName);
            index = (index + 1) % TASK_TRACKER_NUM;
        }

        List<String> TestClssName = new ArrayList();

        TestClssName = convertFormat(subListResult);

        return TestClssName;
    }

    public static List<String> convertFormat(List<List<String>> subListResult) {
        if (subListResult == null) {
            throw new NullPointerException("the subListResult list is null. ");
        }

        List<String> FromatTestClass = new ArrayList<String>();

        // 将子数组拆分成字符串,数组元素间以逗号分隔 e.g.
        // {["test1","test2"],["test3","test4","test5"]} -> ["test1,test2",
        // "test3,test4,test5"]
        for (int j = subListResult.size() - 1; j != -1; j--) {
            FromatTestClass.add(convertListToString(subListResult.get(j)));
        }

        return FromatTestClass;
    }

    /**
     *  将一个list转成字符串形式, ["test1","test2"] -> "test1,test2"
     * @param testNameList
     * @return
     * @author hugang
     */
    public static String convertListToString(List<String> testNameList){
        if(testNameList == null){
            throw new NullPointerException("the testNameList list is null. ");
        }

        String arrayStr = "";
        String subStr;
        for(int i = 0; i < testNameList.size(); i++){
            subStr = testNameList.get(i);
            if(i != testNameList.size() -1){
                arrayStr += (subStr + ",");
            }else{
                arrayStr += subStr;
            }
        }

        return arrayStr;
    }

得到一个list,list每个元素就是每个docker实例需要执行的测试类, 比如有10个测试类”class1,class2,class3,class4,class5,class6,class7,class8,class9,class10”,有5个docker实例,则list形如:[“class1,class2”, “class3,class4”, “class5,class6”, “class7,class8”, “class9,class10”];foreach这个list,将每个元素提交给JobTracker:

    for (String jobTestClass : runTestClassList) {
            submitClassRealtimeJob(jobClient, jobTestClass);
        }


    // 新增测试类参数, 将测试类子集className提交给一个job任务
    private static Response submitClassRealtimeJob(JobClient jobClient,
            String className) {
        Job job = new Job();
        job.setTaskId("run_test_case_realtime_001 " + Math.random());
        job.setParam("className", className);
        job.setTaskTrackerNodeGroup("test_trade_TaskTracker");
        job.setNeedFeedback(true);
        job.setReplaceOnExist(true); // 当任务队列中存在这个任务的时候,是否替换更新
        // 提交任务的状态
        Response response = jobClient.submitJob(job);
        System.out.println(response);
        return response;
    }   

3-3. JobTracker中转站

详见https://github.com/ltsopensource/light-task-scheduler中JobTracker和LTS-Admin(管理后台)部署wiki,需要配zookeeper和mysql,事先要启这2个服务。

3-4.执行用例Docker集群

单个执行用例Docker容器由TaskTracker、测试工程、Java环境、Maven等组成;

Dockerfile如下:

FROM progrium/busybox
MAINTAINER hugang <hugang1010@163.com>

# Install cURL
RUN opkg-install curl

# Java Version
ENV JAVA_VERSION_MAJOR 8
ENV JAVA_VERSION_MINOR 91
ENV JAVA_VERSION_BUILD 14
ENV JAVA_PACKAGE jdk

# Download and unarchive Java
RUN curl -jksSLH "Cookie: oraclelicense=accept-securebackup-cookie"\
  http://download.oracle.com/otn-pub/java/jdk/${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-b${JAVA_VERSION_BUILD}/${JAVA_PACKAGE}-${JAVA_VERSION_MAJOR}u${JAVA_VERSION_MINOR}-linux-x64.tar.gz \
    | gunzip -c - | tar -xf - -C /opt &&\
    ln -s /opt/jdk1.${JAVA_VERSION_MAJOR}.0_${JAVA_VERSION_MINOR} /opt/jdk &&\
    rm -rf /opt/jdk/*src.zip \
           /opt/jdk/lib/missioncontrol \
           /opt/jdk/lib/visualvm \
           /opt/jdk/lib/*javafx* \
           /opt/jdk/jre/lib/plugin.jar \
           /opt/jdk/jre/lib/ext/jfxrt.jar \
           /opt/jdk/jre/bin/javaws \
           /opt/jdk/jre/lib/javaws.jar \
           /opt/jdk/jre/lib/desktop \
           /opt/jdk/jre/plugin \
           /opt/jdk/jre/lib/deploy* \
           /opt/jdk/jre/lib/*javafx* \
           /opt/jdk/jre/lib/*jfx* \
           /opt/jdk/jre/lib/amd64/libdecora_sse.so \
           /opt/jdk/jre/lib/amd64/libprism_*.so \
           /opt/jdk/jre/lib/amd64/libfxplugins.so \
           /opt/jdk/jre/lib/amd64/libglass.so \
           /opt/jdk/jre/lib/amd64/libgstreamer-lite.so \
           /opt/jdk/jre/lib/amd64/libjavafx*.so \
           /opt/jdk/jre/lib/amd64/libjfx*.so


# Set environment
ENV JAVA_HOME /opt/jdk
ENV PATH ${PATH}:${JAVA_HOME}/bin

# maven
ENV MAVEN_VERSION 3.3.9

RUN mkdir -p /usr/share/maven \
  && curl -fsSL http://apache.osuosl.org/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz \
| gunzip -c - | tar -xf - -C /usr/share/maven \
  && ln -s /usr/share/maven/apache-maven-${MAVEN_VERSION}/bin/mvn /usr/bin/mvn

ENV MAVEN_HOME /usr/share/maven

RUN mkdir /FastTest

RUN mkdir /lts-examples

# test cases project
ADD FastTest /FastTest

# tasktracker
ADD lts-examples /lts-examples

# Executer shell
ADD runcase.sh /

# add settings.xml
ADD settings.xml /

WORKDIR /lts-examples/lts-example-tasktracker/lts-example-tasktracker-java/


# CMD run maven
CMD mvn exec:java -Dexec.mainClass="com.github.ltsopensource.example.java.Main" -s /settings.xml

Docker容器使用了busybox基础镜像,该基础镜像占用空间较少;

将测试工程FastTest, lts-examples(TaskTracker), runcase.sh(job任务执行的脚本,执行TestNG), settings.xml(私有仓库等配置等)添加到镜像中;启动docker容器时,同时把TaskTracker任务启起来。

启动容器:

docker run -d --net=host -v /data1/hugang/docker-distributedtest/docker-addresource/lib_path:/lib_path  -it docker-distributed-self 


-v参数指将实体机下/data1/hugang/docker-distributedtest/docker-addresource/lib_path挂载到docker容器/lib_path目录下

docker集群使用swarm, 具体搭建参见:http://www.cnblogs.com/rio2607/p/4445968.html#undefined
https://docs.docker.com/engine/swarm/swarm-tutorial/create-swarm/

3-5.测试结果解析器

直接在测试工程里执行分布式任务,stdout如下:

将每个docker容器执行用例结果进行归并和展示。

4.总结

分布式测试系统维护的成本是比较高的,当执行的用例数在千级以下,是没必要弄这套东西的;如果当你的执行用例数在千级以上,维护分布式测试系统,还是适合的,用空间换时间,减少你的时间成本。

DockerDistributedTest系统还在初级阶段,还有很多优化点,比如回传的结果怎么截取,只传回必要数据;结果可视化等等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值