在我目前和以前的项目中,都遇到了测试运行时间太长,需要把测试分配到多台机器上并行运行的问题。有一些项目在手工的进行测试分配,而本文将介绍如何自动的进行测试分配,从而减少花在这些琐事上的时间。
最开始的问题是什么
随着项目的不断进行,测试会越来越多,而跑一次测试的时间也越来越长,尤其是如果界面测试多的话,那么测试运行时间很容易比较长。以我目前所在的项目为例,基于浏览器的界面测试(以下称为Functional Test)如果单机跑的话大约要80分钟。这样开发人员提交一次代码需要很长时间才能从CI Server上得到结果,而CI反馈周期过长会很大的降低开发人员对CI的关注度,即使仍然关注也会因为不断的切换上下文而降低工作效率。
初始的解决方案
开始时大家会用各种简单的办法来解决这个问题,比如通过tag或annotation(不同的测试框架有不同的概念)手工的把测试分成几部分,然后增加Build agent的数量,让每个Build agent 只跑一部分测试,理想的结果是每个Build agent的测试时间一样长,这样运行测试的时间在理想情况下就是:总的测试时间 / Build agent的数量
但这样同时也带来了新的问题:当增加测试时,设置或修改哪个测试在哪台机器上跑的工作是很琐碎的,我们需要经常关注每个Build agent当前的运行时间是多少, 新的测试加到哪个上面比较合适。我在工作中就经常听到有人说,现在测试太慢了,我们再加一个Build agent, 然后分点测试过去吧。简单重复劳动做的多了,就说明需要关注一下,看看有没有好的办法来解决。好,那就思考一下。
退一步,重新审视问题
每次代码提交后我们需要运行一些Functional Test, 因为测试本身运行时间太长,所以我们需要想办法减少测试时间。相对于换更快的机器来说,增加机器进行并行测试有更好的扩展性,并且成本较低,所以方向应该是没错的。既然方向没有问题,那现在要做的就是要用自动的方式来代替手工分配。而理想的分配方式就是根据时间来进行,这样可以尽量保证不同的机器运行测试的时间相差不大。
现在让我们来明确列出目前要解决的问题:找出当前需要运行的所有测试,根据他们的运行时间,分成几组,在几台机器上并行运行指定的测试,仔细考虑,可以分成以下几个部分:
- 找出所有需要运行的测试?嗯,还不清楚怎么做
- 每个测试的运行时间,这个是已知的,因为我们可以从CI上拿到历史测试数据。对于新增的测试,我们可以用平均的测试时间来估计。
- 分组的数目是确定的,有多少个Build agent 就分几组(配几个就有几个)
- 运行指定的测试,嗯,这个也得google一下
- 分组部分的要求是比较明确地,算法也比较简单输入:一个测试列表,Build agent的数量(分成几组,下面用n表示),历史测试时间处理:根据测试的运行时间分配输出:n组测试列表,每组测试的运行时间尽量相同
我们要做的事情:
下面整理一下我们的分析结果:
已知的东西:
- 分组的数目
- 测试的历史运行时间
需求明确,可以进行的:
- 根据测试时间进行自动分配(已完成,参看https://github.com/theantway/test-distributor)
需要进一步研究的:
- 找到所有要运行的测试
- 运行指定的一组测试
实际上,这两个需要进一步研究的内容需要根据不同的测试框架采用专门的方式来完成。
如果您要把本文介绍的自动分配测试的方式应用到您的CI环境,并且用的是测试框架与本文介绍的相同,那么您就可以直接用了,否则需要针对您使用的测试框架找出上面的最后两点。
使用脚本自动分配测试
通过读取历史运行结果,脚本test_distributor.py 可以把传入的测试列表分成 n份,并且生成n个文件。生成文件的文件名和目录位置都可以通过命令行参数进行配置,每个文件里包含分配好的测试列表。
$ python test_distributor.py --help usage: test_distributor.py [-h] -s [TESTS_HISTORY_REPORT_FOLDER] [-n [BLOCKS]] [-p [RESULT_FILENAME_PREFIX]] [-e [RESULT_FILENAME_EXTENSION]] [-d [DEST_FOLDER]] [--delimiter [DELIMITER]] Assign tests to n blocks for distributed tests. optional arguments: -h, --help show this help message and exit -s [TESTS_HISTORY_REPORT_FOLDER], --source-folder [TESTS_HISTORY_REPORT_FOLDER] source folder which contains history tests reports (default: None) -n [BLOCKS], --blocks [BLOCKS] how many blocks to split (default: 1) -p [RESULT_FILENAME_PREFIX], --name-prefix [RESULT_FILENAME_PREFIX] base name for the result files (default: tests) -e [RESULT_FILENAME_EXTENSION], --name-extension [RESULT_FILENAME_EXTENSION] file extension for the result files (default: txt) -d [DEST_FOLDER], --dest-folder [DEST_FOLDER] destination folder for generated files (default: test- blocks) --delimiter [DELIMITER] delimiter (default: ,)
例如,
在test-blocks目录下生成5个文件:tests-1.txt, tests-2.txt…tests-5.txt。
$ less all_tests | python test_distributor.py –source-folder=HISTORY_REPORTS_FOLDER –-blocks=5
上面的less all_tests是列出所有需要运行的测试.
自动下载需要的文件
为了在CI环境中方便的进行环境搭建,尽量减少手工操作,可以通过脚本自动的从github上下载需要的文件。curl可以在下载前比较远程文件有没有更新,只有当有了新版本的文件时,curl才会真正进行下载,这样下载花的时间也可以忽略:
curl -z ~/test_distributor.py -L https://raw.github.com/theantway/test-distributor/master/test_distributor.py -o ~/test_distributor.py curl -z ~/twist-tests-explorer-1.0.tar.gz -L https://github.com/downloads/theantway/test-distributor/twist-tests-explorer-1.0.tar.gz -o ~/twist-tests-explorer-1.0.tar.gz
通过指定 –z file_name,可以告诉curl, 如果文件没有更改,就不要下载了。这个操作也可以通过wget来进行,这里使用curl是因为在Windows的git bash下自带了curl,而没有wget。
找出Twist的所有测试
如前文所述,需要针对不同的框架使用不同的方式得到测试列表,现在我们看怎样得到Twist的所有测试。
Twist本身并没有提供一个命令行选项来列出所有指定Tag的测试,但因为测试框架自己也需要找出满足条件的测试,于是通过Twist提供的库文件,我们可以比较方便的得到需要的信息。
相应代码在:https://github.com/theantway/test-distributor/tree/master/twist-tests-explorer,
编译好的二进制文件在:https://github.com/theantway/test-distributor/downloads, 下载后解压到test-distributor目录(您也可以解压到其他位置),使用方式是:
$ java -jar test-distributor/twist-tests-explorer-1.0.jar -s FunctionalTests/scenarios -t \'TEAM-FR & !State-In-Progress\'
运行指定的Twist测试
Twist 本身提供了运行指定文件中所有测试的功能,使用方式是调用Twist时指定scenarioListFile=file_name。
在运行ant时,如果指定了测试列表,就只运行指定的测试,否则运行所有的测试,Ant命令为:ant –DscenarioListFile=file_name test,相应的Ant配置如下:
<target name="execute-scenarios" description="Executes scenarios"> <path id="scenarios.classpath"> <path refid="twist.libs" /> <path refid="user.libs" /> <path refid="fixtures.classes" /> </path> <taskdef classname="com.thoughtworks.twist.core.execution.ant.ExecuteScenariosTask" classpathref="scenarios.classpath" name="twist.runner" description="the Twist ant task" /> <if> <isset property="scenarioListFile"/> <then> <fileset dir="${twist.project.dir}/scenarios" includesfile="${scenarioListFile}" id="include.scenarios"/> </then> <else> <fileset dir="${twist.project.dir}/scenarios" includes="**/*.scn" id="include.scenarios"/> </else> </if> <twist.runner scenarioDir="${twist.project.dir}/scenarios" reportsDir="${twist.reports.output.dir}" confDir="${twist.config.dir}" failureproperty="twist.scenarios.failed" classpathref="scenarios.classpath" tags="${twisttags}" threads="1"> <fileset refid="include.scenarios"/> </twist.runner> <fail if="twist.scenarios.failed" message="One or more scenarios for failed" /> </target>
更多信息请参考Twist的官方帮助文档(如何配置Ant, Maven, Rake)
在Jenkins 上配置Twist 自动分配测试
最后让我们把测试分成3份,并在Jenkins上把整个过程配置完成
最终的job关系图如下:
配置job的具体步骤是:
- 创建Compile Job(提交代码后的跑的第一个Job, 进行编译和单元测试), 配置这个Job进行测试的分配如图增加一个Build Step, 执行Shell script:
curl -z ~/test_distributor.py -L https://raw.github.com/theantway/test-distributor/master/test_distributor.py -o ~/test_distributor.py curl -z ~/twist-tests-explorer-1.0.tar.gz -L https://github.com/downloads/theantway/test-distributor/twist-tests-explorer-1.0.tar.gz -o ~/twist-tests-explorer-1.0.tar.gz mkdir -p test-distributor && tar -xzf ~/twist-tests-explorer-1.0.tar.gz -C test-distributor java -jar test-distributor/twist-tests-explorer-1.0.jar -s FunctionalTests/scenarios -t \'TEAM-FR & !State-In-Progress\' | python ~/test_distributor.py --source-folder=previous-test-reports --blocks=5 --delimiter=\'n\'
指定Archive artifacts 包含生成的文件: test-blocks/*.txt
- 创建Multi-configuration 类型的Functional-Tests Job, 运行分配的部分测试,生成并保存Junit test 兼容的测试报告增加一个User Defined Axis, Name: testblock, Values: 1 2 3增加一个Build Step, 选择Copy Artifact(需要单独的插件支持),选择从Compile Job复制Artifacts,这样我们就得到了分配好的测试列表执行测试:ant –DscenarioListFile=tests-$testblock.txt test。然后再指定Archive artifacts 包含生成的测试报告: test-reports/**/*.xml
- 修改Compile Job, 从Functional Tests 中获取Test Report, 从而不断根据最新的测试时间对测试分配进行调整增加一个Build Step, 选择Copy Artifact,选择从Functional-Tests Job复制Artifacts,这样我们就得到了历史测试记录。我们需要选上Optional 选项,因为第一次运行时还没有Artifacts
Jenkins上的测试结果页面效果
参考:
- 关于Jenkins上如何配置Multi-configuration Job的介绍:Using multi-configuration project for distributed builds on Jenkins
- 本文代码库:https://github.com/theantway/test-distributor