我想在多个线程中运行相同的Cucumber测试。更具体地说,我有一组功能,在一个线程中运行这些功能工作正常。我使用JSON格式化程序来记录每个步骤的运行时间。现在我想做负载测试。我更关心多线程环境中每个功能/步骤的运行时间。所以我创建了多个线程,每个线程都运行在同一个功能集上。每个线程都有自己的JSON报告。理论上这可能吗?
对于某些项目设置原因,我无法使用JUnit运行程序。所以我不得不采用CLI方式:
long threadId = Thread.currentThread().getId();
String jsonFilename = String.format("json:run/cucumber%d.json", threadId);
String argv[] = new String[]{
"--glue",
"com.some.package",
"--format",
jsonFilename,
"d:\\features"};
// Do not call Main.run() directly. It has a System.exit() call at the end.
// Main.run(argv, Thread.currentThread().getContextClassLoader());
// Copied the same code from Main.run().
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
RuntimeOptions runtimeOptions = new RuntimeOptions(new Env("cucumber-jvm"), argv);
ResourceLoader resourceLoader = new MultiLoader(classLoader);
ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
Runtime runtime = new Runtime(resourceLoader, classFinder, classLoader, runtimeOptions);
runtime.writeStepdefsJson();
runtime.run();
我试图为每个Cucumber运行创建一个单独的线程。问题是,只有一个线程有一个有效的JSON报告。所有其他线程只创建空JSON文件。这是黄瓜的设计还是我错过了什么?
我们使用优秀的GPars library研究了Gradle和Groovy下的多线程黄瓜测试。我们有650个UI测试和计数。
我们没有遇到任何在多线程中运行cucumber-JVM的明显问题,但多线程也没有像我们希望的那样提高性能。
我们在一个单独的线程中运行每个功能文件。有一些细节要处理,比如拼接来自不同线程的黄瓜报告,并确保我们的步骤代码是线程安全的。我们有时需要在步骤之间存储值,因此我们使用了一个与线程id相关联的concurrentHashMap来存储这种数据:
class ThreadedStorage {
static private ConcurrentHashMap multiThreadedStorage = [:]
static private String threadSafeKey(unThreadSafeKey) {
def threadId = Thread.currentThread().toString()
"$threadId:$unThreadSafeKey"
}
static private void threadSafeStore(key, value) {
multiThreadedStorage[threadSafeKey(key)] = value
}
def static private threadSafeRetrieve(key) {
multiThreadedStorage[threadSafeKey(key)]
}
}
这里是使用GPars运行多线程测试的Gradle任务代码的要点:
def group = new DefaultPGroup(maxSimultaneousThreads())
def workUnits = features.collect { File featureFile ->
group.task {
try {
javaexec {
main = "cucumber.api.cli.Main"
...
args = [
...
'--plugin', "json:$unitReportDir/${featureFile.name}.json",
...
'--glue', 'src/test/groovy/steps',
"path/to/$featureFile"
]
}
} catch (ExecException e) {
++noOfErrors
stackTraces << [featureFile, e.getStackTrace()]
}
}
}
// ensure all tests have run before reporting and finishing gradle task
workUnits*.join()
我们发现我们需要以与执行时间相反的顺序显示要素文件,以获得最佳结果。
结果是i5 CPU的性能提高了30%,降低了4个以上的同步线程,这有点令人失望。
我认为线程太重了,无法在我们的硬件上进行多线程处理。超过一定数量的线程有太多的CPU缓存未命中。
现在使用像Amazon SQS这样的线程安全工作队列在不同实例上并发运行似乎是一个很好的方法,特别是因为它不会遇到线程安全问题(至少在测试框架方面没有)。
由于我们工作场所的安全限制,我们在i7硬件上测试这种多线程方法并非易事,但我很想知道具有更大CPU缓存和更多物理内核的i7如何比较。
据说你可以从这里使用这个Maven POM配置并行运行你的Cucumber-JVM测试:https://opencredo.com/running-cucumber-jvm-tests-in-parallel/
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.14</version>
<executions>
<execution>
<id>acceptance-test</id>
<phase>integration-test</phase>
<goals>
<goal>test</goal>
</goals>
<configuration>
<forkCount>${surefire.fork.count}</forkCount>
<refuseForks>false</reuseForks>
<argLine>-Duser.language=en</argLine>
<argLine>-Xmx1024m</argLine>
<argLine>-XX:MaxPermSize=256m</argLine>
<argLine>-Dfile.encoding=UTF-8</argLine>
<useFile>false</useFile>
<includes>
<include>**/*AT.class</include>
</includes>
<testFailureIgnore>true</testFailureIgnore>
</configuration>
</execution>
</executions>
</plugin>
在上面的代码片段中,您可以看到maven-surefire-plugin用于运行我们的验收测试 - 任何以* AT结尾的类都将作为JUnit测试类运行。感谢JUnit,让测试并行运行现在是设置forkCount配置选项的一个简单例子。在示例项目中,它设置为5,这意味着我们一次最多可以运行5个线程(即5个运行程序类)。
好吧,如果你能找到一种方法让黄瓜输出基于给定标签运行的所有场景的场景位置(即feature_file_path:line_nunber_in_feature_file),那么你可以使用gpars和gradle并行运行场景。步骤1:在第一个gradle任务中,我们将使用上面的解决方案生成一个文本文件(比如scenarios.txt),其中包含我们要执行的所有场景的位置。步骤2:接下来,提取在scene.txt中生成的scenario.txt的内容第1步进入一个groovy列表说scenarioList第3步:再创建一个任务(javaExec),这里我们将使用gpars withPool结合scenarioList.eachParallel,并使用cucumber主类和其他cucumberOptions来并行运行这些场景。 PS:这里我们将提供一个场景位置作为选项“features”的值,以便黄瓜只运行此场景。也不需要提供任何标记名称,因为我们已经有一个我们需要执行的场景列表。
注意:您需要使用具有高配置的计算机(如Linux服务器),因为每个方案都会创建一个新的jvm实例,并且可能使用像Saucelabs这样的云服务来执行方案。这样您就不必担心基础架构了。
第4步:这是最后一步。步骤3中的每个方案Bran都将生成一个json输出文件。您必须根据功能名称整理输出,以便为每个功能文件生成一个json文件。
这个解决方案听起来有点复杂,但通过正确的努力可以产生显着的效果。