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 的设计还是我错过了什么?

4

4 回答 4

2

我们使用优秀的GPars 库研究了 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)]
    }


}

以下是使用 GPar 运行多线程测试的 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 相比如何。

于 2016-04-13T02:51:01.330 回答
1

目前不是 - 这是您观察到的问题。我还没有找到任何按场景并行化的方法。

这是一篇关于穷人并发的好文章。只需运行多个命令,每个命令选择不同的测试子集——按功能或标签。我会 fork 一个新的 JVM(就像 JUnit 驱动程序那样),而不是尝试线程化它,因为 cucumber 不是为此而设计的。您必须自己平衡它们,然后弄清楚如何组合报告。(但至少问题在于合并报告而不是损坏报告。)

于 2015-04-15T05:42:10.843 回答
0

假设您可以使用此处的 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 个运行器类)。

于 2016-11-09T20:16:40.917 回答
-1

好吧,如果您可以找到一种方法让 cucumber 根据给定标签为您想要运行的所有场景输出场景位置(即 feature_file_path:line_nunber_in_feature_file),那么您可以使用 gpars 和 gradle 并行运行场景。第 1 步:在第一个 gradle 任务中,我们将使用上述解决方案生成一个文本文件(例如,scenarios.txt),其中包含我们要执行的所有场景的位置 第 2 步:接下来,提取在第 1 步进入一个 groovy 列表,比如场景列表 第 3 步:再创建一个任务(javaExec),这里我们将结合使用 gpars withPool 和场景列表.eachParallel,并使用 cucumber 主类和其他 cucumberOptions 来并行运行这些场景。PS:这里我们会提供一个场景位置作为选项“features”的值,这样cucumber就只会运行这个场景。

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

Step4:这是最后一步。步骤 3 中的每个场景麸皮都会生成一个 json 输出文件。您必须根据功能名称整理输出,以便为每个功能文件生成一个 json 文件。

这个解决方案听起来有点复杂,但只要努力就可以产生显着的效果。

于 2019-04-13T04:33:45.413 回答