Cucumber-jvm是线程安全的吗?

7
我希望能够在多个线程中运行相同的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文件。这是Cucumber的设计问题还是我错过了什么?


我曾经创建了这个项目:https://github.com/mrunalgosar/cucumber-jvm 以便并行运行特性文件。但是,由于它基于JUnitRunners,因此拉取请求未被批准。 - Mrunal Gosar
4个回答

2

我们研究了在Gradle和Groovy下使用优秀的GPars库进行多线程cucumber测试。我们有650个UI测试案例,并且还在继续添加。

我们在多线程中运行cucumber-JVM时没有遇到任何明显的问题,但是多线程也没有像我们希望的那样提高性能。

我们将每个功能文件放在单独的线程中运行。需要注意一些细节,例如从不同线程拼接cucumber报告并确保我们的步骤代码是线程安全的。有时我们需要在步骤之间存储值,因此我们使用一个以线程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的表现如何。

1

目前还不行——你观察到的问题在这里。我没有找到任何按场景并行化的方法。

这是一个关于穷人并发的好文章。只需运行多个命令,每个命令选择不同的测试子集——按特性或标签分类。我会分叉一个新的JVM(就像JUnit驱动程序一样),而不是尝试线程化,因为Cucumber并不是为此设计的。你必须自己平衡它们,然后想办法将报告组合起来。(但至少问题是组合报告而不是损坏报告。)


0

据说您可以使用此处的Maven POM配置https://opencredo.com/running-cucumber-jvm-tests-in-parallel/来并行运行Cucumber-JVM测试。

<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个运行器类)。

-1

如果你能找到一种方法让cucumber输出所有想要运行的场景的位置(例如feature_file_path:line_nunber_in_feature_file)基于给定的标签,那么你可以使用gpars和gradle来并行运行场景。 第一步:在第一个gradle任务中,我们将使用上述解决方案生成一个文本文件(称为scenarios.txt),其中包含我们要执行的所有场景的位置。 第二步:接下来,提取步骤1生成的scenarios.txt文件的内容,并放入一个groovy列表中,名为scenariosList。 第三步:创建另一个任务(javaExec), 这里我们将使用GPars withPool与scenariosList.eachParallel相结合,并使用cucumber main类和其他cucumberOptions并行运行这些场景。备注:在此我们将提供一个场景位置作为选项“features”的值,以便cucumber仅运行此场景。由于我们已经有了需要执行的场景列表,因此无需提供任何标记名称。

注意:您需要使用高配置的机器,如Linux服务器,因为每个场景都会创建一个新的JVM实例,并可能使用云服务(例如Saucelabs)执行场景。这样您就不用担心基础设施的问题。

步骤4:这是最后一步。在第3步中创建的每个场景分支都将生成一个JSON输出文件。您必须根据功能名称整理输出,以便为每个功能文件生成一个JSON文件。

这个解决方案听起来有点复杂,但只要付出正确的努力,就可以产生显著的结果。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接