首页 > 解决方案 > Spock 中 TestNG 数据提供者的模拟

问题描述

我是 Spock 的新手,目前正在切换到它,但我继承了许多需要重复使用的测试配置文件。每个配置文件都是一个 JSON,与 Spec 类同名。对于每种测试方法,都有一个带有参数的映射列表,例如:

LoginSpec.json:
{
  "My first test": [
    {
      "user": "user_one",
      "role": "ADMIN"
    },
    {
      "user": "user_two",
      "role": "REPORTER",
      "other_param": "other"
    }
  ],

  "Some Other Test Method": [
    {
      "url": "/lab1",
      "button_name": "Show news popup"
    }
  ]
}

TestNG允许我在数据提供者方法中传递测试方法名称,因此我可以根据测试类名称和测试方法名称返回映射列表。我的基类中只有一个数据提供者方法:

public Object[][] getData(String method) {
    DataReader reader = new JsonReader()
    return reader.parse(packageFullName, getClass().simpleName, method)
}

作为这种方法的结果,我得到了一个 Maps 数组以在每次测试迭代中使用。然后我只是将此方法指定为 DataProvider:

@Test(dataProvider = "getData", priority = 1)
void EULA_1(Map data) { <====
    Pages.login.openLoginPage()
    Pages.login.logIn(data.user) <====
    ...
} 

这完美地工作:在基类中声明,它会自动接收测试并提供测试数据。

问题是:有没有办法在 Spock 测试中应用类似的方法?

我想在我的基类中有一些 getData() 方法,在那里我可以根据测试方法名称读取测试参数,然后将它们传递到where块。

我尝试使用我的 json 阅读器,如下所示:

def "My first test"() {
    setup:
    println(data)

    when:
    ...
    then:
    ...

    where:
    data = dataReader.parse("JobE2E", "LoginSpec.json", "My first test")
}

此示例为我提供了所需的地图列表,但有两个问题:

  1. 此处的数据- 是完整的地图列表,而不是每次迭代的地图;
  2. 我被迫显式输入测试方法、类等的名称。

总结: 实现接收测试方法名称并返回地图列表的数据提供者的最佳方法是什么?

标签: javatestinggroovytestngspock

解决方案


data您可以使用这种方法解决问题:

data << dataReader.parse('JobE2E', "${getClass().name}.json", 'My first test')

它将迭代映射列表,因此每个测试迭代将仅由该映射参数化。


当前测试名称可以通过以下方式获得:

specificationContext.currentFeature.name

当前迭代名称为:

specificationContext.currentIteration.name

但是在该部分中两者都不可访问,where因为它是在测试本身之前执行的,其中只有来自共享上下文的值可用。所以这里恐怕你必须手动输入测试名称。

更新:where我找到了如何在部分中为您获取功能名称的解决方案。它是通过使用拦截器的自己的扩展来实现的。

功能详细信息容器:

class FeatureDetails {
    String name
}

扩展注释:

import org.spockframework.runtime.extension.ExtensionAnnotation

import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ExtensionAnnotation(FeatureDetailsExtension.class)
@interface ShareFeatureDetails {
}

带有内联拦截器实现的 Spock 扩展:

import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension
import org.spockframework.runtime.model.FeatureInfo

class FeatureDetailsExtension extends AbstractAnnotationDrivenExtension<ShareFeatureDetails> {
    def featureDetails = new FeatureDetails()

    @Override
    void visitFeatureAnnotation(ShareFeatureDetails annotation, FeatureInfo feature) {
        feature.addInterceptor({ i ->
            featureDetails.name = feature.name
            feature.spec.allFields.each { f ->
                if (f.type == FeatureDetails.class && f.readValue(i.getInstance()) == null) {
                    f.writeValue(i.getInstance(), featureDetails)
                }
            }
            i.proceed()
        })
    }
}

扩展的示例用法:

class DataProviderSpec extends Specification {
    @Shared
    FeatureDetails currentFeature

    @Unroll("Test #data.a * 2 = #data.b")
    @ShareFeatureDetails
    def 'test'() {
        when:
        println data

        then:
        data.a * 2 == data.b

        where:
        data << loadData()
    }

    @Unroll("Test #data.a * 3 = #data.b")
    @ShareFeatureDetails
    def 'another test'() {
        when:
        println data

        then:
        data.a * 3 == data.b

        where:
        data << loadData()
    }

    def loadData() {
        // this is hard coded example
        println "${getClass().name}.${currentFeature.name}"
        if ('test' == currentFeature.name) return [[a: 1, b: 2], [a: 2, b: 4]]
        if ('another test' == currentFeature.name) return [[a: 3, b: 9], [a: 4, b: 12]]
        return []
        // ... use load from data file (JSON, YAML, XML, ...) instead:
        // return dataReader.parse("${getClass().name}.json", currentFeature.name)
    }
}

以上示例的输出:

DataProviderSpec.test
[a:1, b:2]
[a:2, b:4]
DataProviderSpec.another 测试
[a:3, b:6]
[a:4, b:8]

第一个想法是String featureName在规范类中仅使用带注释的字段,但存在一个问题,即visitFeatureAnnotation()方法在每次调用期间使用不同的规范实例,而loadData()每次在第一个实例上执行方法。


注意:您还可以使用@Unroll注释添加具有特定于当前迭代的值的描述。例如:

@Unroll("Test #data.a * 2 = #data.b")
def 'test'() {
    setup:
    ...
    when:
    ...
    then:
    data.a * 2 == data.b

    where:
    data << getData('test')
}

def getData(String methodName) {
    if ('test' == methodName) return [[a: 1, b: 2], [a: 2, b: 4]]
    ...
}

将产生:

测试 1 * 2 = 2
测试 2 * 2 = 4


推荐阅读