当自动化用例累积的越来越多,回归自动化用例的时间越来越长。
我们往往会选择使用多线程的方式来跑用例集,但是用例数量达到一定数量级(千级以上)后,在单台机器上使用多线程 (千级以上) 直接影响到机器性能,能不能组成并行加并发的模式跑用例,自动将用例集拆分成更细粒度的子集,将子集在单独的容器(容器可以部署在多台机器上)内并发执行。
参见:
https://github.com/SeleniumHQ/selenium/wiki/Grid2
参见:
https://jmeter.apache.org/usermanual/jmeter_distributed_testing_step_by_step.pdf
http://jmeter.apache.org/usermanual/remote-test.html
参见:
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 的便利,现在开发的生产环境已开始大规模使用,而自动化测试领域也必将有这个趋势。
最终形成了基于 Docker 集群的分布式测试系统 DDT(DockerDistributedTest);
该系统使用 Docker 容器作为子集的执行容器,Docker 镜像中打包了用例所需运行环境 (Java 环境)、测试工程及分布式组件 TaskTrack,并将测试工程依赖的 jar 包挂载到 Docker 容器中。
我们自动化测试工程是 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)
早在 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/,由于是非官方的,并且也不是很完善,我们没采用他们的方式。
入门简单,由 worker、client 和 Job server 组成,client 发送任务给 Job Server,Job Server 将任务传送给 worker 执行并将结果返回给 client(http://gearman.org/getting-started/);尝试过,但是不稳定,外网机器调度内网机器或内网机器调度内网机器,会发生无法执行任务的情况,猜测可能是 Gearman Protocol(http://gearman.org/protocol/) 被公司网络限制。
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. 安装时,依赖的一些资源,国内无法下载,故没安装上; 项目也没怎么维护。
Python 开发的一款分布式任务队列(https://github.com/celery/celery),用的人还是蛮多的,github 上 5k+ 个 star,因为我们的工程是 java 编写,最好使用有 java api 的工具,故没使用该工具。
Java 开发的一款分布式任务调度工具 (https://github.com/ltsopensource/light-task-scheduler),提供了完整的文档、使用示例和前端控制台,并且有丰富的 java api,可以将调度方有效的结合到测试工程中;故我们使用 LTS 作为我们分布式测试调度工具; LTS 主要由 JobClient(负责提交任务,并接受结果)、JobTracker(接收并分配任务)、TaskTracker(执行任务,结果反馈给 JobTrack) 组成,同时需要配置 zookeeper 和 mysql, 详见 github 主页 LTS 用户文档。
完整的流程图:
DockerDistributedTest 系统由自动化测试工程、用例子集发送器(将测试集自动拆成 N 个子集)、JobTrack 中转站、执行用例 Docker 集群、测试结果解析器组件构成。
自动化测试工程 FastTest, 用例使用 TestNG 编写,支持 http 接口测试、rpc 服务测试等。
用例子集发送器由 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;
}
详见https://github.com/ltsopensource/light-task-scheduler中 JobTracker 和 LTS-Admin(管理后台)部署 wiki,需要配 zookeeper 和 mysql,事先要启这 2 个服务。
单个执行用例 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/
直接在测试工程里执行分布式任务,stdout 如下:
将每个 docker 容器执行用例结果进行归并和展示。
分布式测试系统维护的成本是比较高的,当执行的用例数在千级以下,是没必要弄这套东西的;如果当你的执行用例数在千级以上,维护分布式测试系统,还是适合的,用空间换时间,减少你的时间成本。
DockerDistributedTest 系统还在初级阶段,还有很多优化点,比如回传的结果怎么截取,只传回必要数据;结果可视化等等。