串行测试 并行测试
by Karel Rochelt
卡雷尔·罗切尔特(Karel Rochelt)
Providing an error-free API for a heavily developed project is not an easy task. Likely, the first things that come to mind are tests. For a mid-sized API, you may write hundreds or even thousands of end-to-end tests. These tests significantly prolong build times.
为高度开发的项目提供无错误的API并非易事。 可能想到的第一件事就是测试。 对于中型API,您可以编写数百甚至数千个端到端测试。 这些测试大大延长了构建时间。
In this post, we will explain how we solved long build times with CircleCI test parallelism and Gradle/Grails for Amio main service.
在这篇文章中,我们将说明如何使用CircleCI测试并行性和用于Amio主服务的Gradle / Grails解决较长的构建时间。
CircleCI’s documentation does a decent job of explaining how their command line interface (CLI) tool should be used to enable test parallelism. When I started looking into it for the first time, it wasn’t entirely obvious what the returned result would look like. I was asking myself, “So, I’ll run this command and it will magically start splitting my tests?” Well, of course not! The result is a list of test files that should be executed on a particular container.
CircleCI的文档在解释如何使用其命令行界面 (CLI)工具来启用测试并行性方面做得不错。 当我第一次开始研究它时,返回的结果看起来并不完全是显而易见的。 我问自己:“那么,我将运行此命令,它将神奇地开始拆分测试?” 好吧,当然不会! 结果是应在特定容器上执行的测试文件的列表。
Does that sound complicated? Let me explain in an example.
这听起来复杂吗? 让我举例说明。
The first thing we have to do is to set the parallelism key in the .circleci/config.yml
file. From the CircleCI docs:
我们要做的第一件事是在.circleci/config.yml
文件中设置并行键。 从CircleCI文档:
The
parallelism
key specifies how many independent executors will be set up to run the steps of a job.
parallelism
键指定将设置多少个独立执行器来运行作业的各个步骤。
Any value greater than one will enable parallel execution, but for the sake of this example, let’s go with two. This way, every time a CircleCI job is started, it will spawn two containers which will both do the same tasks.
任何大于1的值都将允许并行执行,但就本示例而言,让我们处理2。 这样,每次启动CircleCI作业时,它将产生两个容器,它们将执行相同的任务。
If we were to use the parallelism key with no additional configuration, it would just run all of our tests twice. That is not what we want. We want to split our tests between the containers.
如果我们使用并行键而不进行其他配置,则它将只运行两次所有测试。 那不是我们想要的。 我们想在容器之间拆分测试。
That’s where the CircleCI CLI comes in. It offers two commands which, when used together, split our tests into equal portions across our two containers.
这就是CircleCI CLI的用处。它提供了两个命令,这些命令一起使用时,会将我们的测试分成两个容器,分成相等的部分。
Let’s say these are the test files in our project:
假设这些是我们项目中的测试文件:
src/integration-test/groovy/com/package1/Test1.groovysrc/integration-test/groovy/com/package1/Test2.groovysrc/integration-test/groovy/com/package2/Test3.groovysrc/integration-test/groovy/com/package2/Test4.groovysrc/integration-test/groovy/com/package2/Test5.groovy
Naturally, we will have other source files in our project; not just our tests. They may be located in the same src/integration-test/…
directory. To achieve our goal of test splitting, we need to select only the test files for the project. That is done by using the glob command:
当然,我们的项目中将包含其他源文件。 不只是我们的测试。 它们可能位于相同的src/integration-test/…
目录中。 为了实现我们的测试拆分目标,我们只需要为项目选择测试文件。 这可以通过使用glob命令来完成:
circleci tests glob "src/integration-test/**/*.groovy"
This command will output the list of our tests (all 5 of them). ? Now we use the split command to, well, split them between containers:
此命令将输出我们的测试列表(全部5个)。 ? 现在,我们使用split命令在容器之间分割它们:
circleci tests glob "src/integration-test/**/*.groovy" | circleci tests split --split-by=timings
The split
command offers several strategies to split the tests but timings
is my favorite. It uses the timings data that is collected by CircleCI (this has to be enabled via the store_test_results key) to split the tests into portions that take a similar time to execute. Container indexing is automatic. We can run the same command on every container. In our example, running the command on Container 0 might output:
split
命令提供了几种拆分测试的策略,但是timings
是我的最爱。 它使用CircleCI收集的计时数据(必须通过store_test_results键启用此计时数据)将测试分成需要花费相似时间执行的部分。 容器索引是自动的。 我们可以在每个容器上运行相同的命令。 在我们的示例中,在容器0上运行命令可能会输出:
src/integration-test/groovy/com/package1/Test1.groovysrc/integration-test/groovy/com/package2/Test3.groovy
And on Container 1:
在容器1上:
src/integration-test/groovy/com/package1/Test2.groovysrc/integration-test/groovy/com/package2/Test4.groovysrc/integration-test/groovy/com/package2/Test5.groovy
I say “might” because the real result would depend on the timings data. As you can see, every container got its half of the tests.
我说“可能”是因为实际结果将取决于计时数据。 如您所见,每个容器都进行了一半的测试。
Splitting the tests in CircleCI was the easy part. The hard part is getting Gradle to execute just the tests that are in the result of the split command. If we were using JavaScript and Mocha, it would be much easier. Mocha accepts a list of files which should be executed. With Gradle 3, I had been using this command to run tests: ./gradlew check -i
在CircleCI中拆分测试很容易。 困难的部分是让Gradle仅执行split命令结果中的测试。 如果我们使用JavaScript和Mocha,那会容易得多。 Mocha接受应执行的文件列表。 在Gradle 3中,我一直使用此命令来运行测试: ./gradlew check -i
Gradle’s documentation isn’t really helpful. Just figuring out what the check task does is a pain. Thankfully, it is possible to pass our test list as a parameter to the Gradle task.
Gradle的文档并没有真正帮助。 仅弄清楚检查任务的作用是很痛苦的。 幸运的是,可以将我们的测试列表作为参数传递给Gradle任务。
./gradlew check -i -PtestFilter="`circleci tests glob "src/integration-test/**/*.groovy" | circleci tests split --split-by=timings`"
Now, when the check
task is started, it has access to the testFilter
parameter. To make everything work, we also need to add some code that can handle the parameter in our build.gradle:
现在,当check
任务启动时,它可以访问testFilter
参数。 为了使一切正常,我们还需要在build.gradle中添加一些可以处理参数的代码 :
integrationTest { if (project.hasProperty("testFilter")) { List<String> props = project.getProperties().get("testFilter").split("\\s+") props.each { include(it.replace("src/integration-test/groovy/com/", "**/").replace(".groovy", ".class")) } }}
Note that the parameter was passed to the task as a single string. In the code block above, Line 3 contains logic to split it back into rows. Calling include will tell Gradle to execute only the tests that we include. Now we can include all the rows, and we’re good, right?
请注意,该参数已作为单个字符串传递给任务。 在上面的代码块中,第3行包含将其拆分回行的逻辑。 调用include将告诉Gradle仅执行我们包含的测试。 现在我们可以包括所有行了,我们很好,对吧?
Nope. Gradle doesn’t know how to work with source files. It only understands classes. We need to pass the compiled class files to it.
不。 Gradle不知道如何使用源文件。 它只了解类。 我们需要将编译后的类文件传递给它。
There are two problems with that. First, the compiled classes are not in the same directory. Second, the suffix is not .groovy but .class.
这有两个问题。 首先,已编译的类不在同一目录中。 其次,后缀不是.groovy而是.class。
To overcome the first problem, we replaced the common prefix with **/. This says, “Look in the root directory and all its subdirectories.” Of course, you could replace it with something like build/classes/integrationTest/com
. That is cleaner, but not necessary. This should be safe as long as the test classes names are unique. Line 5 in the code block above includes logic that solves both of these problems.
为了解决第一个问题,我们将通用前缀替换为** /。 它说:“查看根目录及其所有子目录。” 当然,您可以将其替换为build/classes/integrationTest/com
。 那比较干净,但不是必需的。 只要测试类名称是唯一的,这应该是安全的。 上面代码块中的第5行包含解决了这两个问题的逻辑。
In the end, your .circleci/config.yml
should look something like this (just the relevant part):
最后,您的.circleci/config.yml
应该看起来像这样(只是相关部分):
- run: # This is just for debugging purposes, you can omit this step name: test splitting output command: circleci tests glob “src/integration-test/**/*.groovy” | circleci tests split --split-by=timings | xargs -n 1 echo
- run: name: test command: ./gradlew check -i -PtestFilter="`circleci tests glob “src/integration-test/**/*.groovy” | circleci tests split --split-by=timings`"
And that’s it! Easy, right? Well, it was a bit more work than it should have been. Having our test times cut nearly in half was definitely worth it! Applying test parallelism, we’ve decreased the build time from around 15 minutes to 9 minutes.
就是这样! 容易吧? 好吧,这比应做的工作多了一点。 我们的测试时间缩短了将近一半绝对值得! 应用测试并行性,我们将构建时间从大约15分钟减少到了9分钟。
串行测试 并行测试