自动化工具 基于 Docker 集群的分布式测试系统 DDT (DockerDistributedTest)

胡刚 · 2016年08月19日 · 最后由 胡刚 回复于 2017年10月25日 · 2744 次阅读
本帖已被设为精华帖!

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 条回复 时间 点赞
Monkey 将本帖设为了精华贴 08月19日 17:25

@neven7 没想到几年前写的 Blog 还被你发现了,呵呵。当年我们也是在做一个类似的项目,用 Docker 来做分布式的执行引擎,我们维护了一个简化支持分布式的 TestNG 的版本。由于把序列化的部分给注释掉了,所以我们是每一个 slave contaienr 会生成一个 result.xml,然后通过一个脚本把这些小的 xml 合并成一个大的 xml,再交给 Jenkins 来解析,我目前正在带团队做一个全新的分布式测试平台,有机会的话希望可以和你有更过的交流和探讨。这个是我们那个时候维护的 TestNG 版本https://github.com/TestFoundry/testng,有兴趣的同学可以参考。

不错,技术好文,要好好研究下

—— 来自 TesterHome 官方 安卓客户端

胡刚 #10 · 2016年08月22日 Author

#2 楼 @cesc 幸会,可以加我 QQ:591275572,聊聊。

不错,现在也考虑将 docker 引入

—— 来自 TesterHome 官方 安卓客户端

airong [该话题已被删除] 中提及了此贴 10月22日 17:33

技术好文,赞

赞一个,不让任务排队成为测试的瓶颈

写的很不错呢,各个软件的优缺点都介绍了下……

airong 基于 Docker 的分布式测试系统构建 (二) 中提及了此贴 12月23日 11:16

想问一下 ‘并行加并发’ 的意思是?

胡刚 #13 · 2017年10月25日 Author
xixixi 回复

并行多个机器执行,并发每台机器上多线程执行。

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册