为可能的空数组定义合同?

Define contract for possible empty array?

我正在尝试使用 Spring-Cloud-Contract 定义 CDC 合同,如下所示:

org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'GET'
        url $(client(~/\/categories\?publication=[a-zA-Z-_]+?/), server('/categories?publication=DMO'))
    }
    response {
        status 200
        headers {
            header('Content-Type', 'application/json;charset=UTF-8')
        }
        body """\
            [{
                "code": "${value(client('DagKrant'), server(~/[a-zA-Z0-9_-]*/))}",
                "name": "${value(client('De Morgen Krant'), server(~/[a-zA-Z0-9_\- ]*/))}",
                "sections" : []
            },
            {
                "code": "${value(client('WeekendKrant'), server(~/[a-zA-Z0-9_-]*/))}",
                "name": "${value(client('De Morgen Weekend'), server(~/[a-zA-Z0-9_\- ]*/))}",
                "sections" : [
                    {
                    "id" : "${value(client('a984e824'), server(~/[0-9a-f]{8}/))}",
                    "name" : "${value(client('Binnenland'), server(~/[a-zA-Z0-9_\- ]*/))}"
                    }
                ]
            }]
        """
    }
}

在生成的测试中,这会导致以下断言:

DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).array().contains("code").matches("[a-zA-Z0-9_-]*");
assertThatJson(parsedJson).array().array("sections").contains("id").matches("([0-9a-f]{8})?");
assertThatJson(parsedJson).array().array("sections").contains("name").matches("[a-zA-Z0-9_\- ]*");
assertThatJson(parsedJson).array().contains("name").matches("[a-zA-Z0-9_\- ]*");

但在我的测试中,我想允许 sections 数组为空,就像第一个示例一样。现在,如果我的测试实现 returns 一个空的部分数组,生成的测试将失败,因为它找不到空数组的部分 ID。

Parsed JSON [[{"code":"WeekendKrant","name":"De Morgen Weekend","sections":[]}]] 
doesn't match the JSON path [$[*].sections[*][?(@.id =~ /([0-9a-f]{8})?/)]]

我也尝试过使用 optional(),但唯一的区别是正则表达式包含一个“?”在最后。 JSON 断言仍然失败。

在存根中,两个结果都返回了,但是对于测试,我也希望两个结果都成功。测试断言是否纯粹在每个属性的最后一次出现时生成?数组上不可能有类似 'optional()' 的东西吗?

在 1.0.3.RELEASE 版本之前,无法进行像这样的额外检查。从那个版本开始,您可以提供额外的匹配器 - http://cloud.spring.io/spring-cloud-static/spring-cloud-contract/1.0.3.RELEASE/#_dynamic_properties_in_matchers_sections 。您可以将 byType 与与尺寸相关的附加检查相匹配。

摘自文档:

Currently we support only JSON Path based matchers with the following matching possibilities. For stubMatchers:

byEquality() - the value taken from the response via the provided JSON Path needs to be equal to the provided value in the contract

byRegex(…​) - the value taken from the response via the provided JSON Path needs to match the regex

byDate() - the value taken from the response via the provided JSON Path needs to match the regex for ISO Date

byTimestamp() - the value taken from the response via the provided JSON Path needs to match the regex for ISO DateTime

byTime() - the value taken from the response via the provided JSON Path needs to match the regex for ISO Time

For testMatchers:

byEquality() - the value taken from the response via the provided JSON Path needs to be equal to the provided value in the contract

byRegex(…​) - the value taken from the response via the provided JSON Path needs to match the regex

byDate() - the value taken from the response via the provided JSON Path needs to match the regex for ISO Date

byTimestamp() - the value taken from the response via the provided JSON Path needs to match the regex for ISO DateTime

byTime() - the value taken from the response via the provided JSON Path needs to match the regex for ISO Time

byType() - the value taken from the response via the provided JSON Path needs to be of the same type as the type defined in the body of the response in the contract. byType can take a closure where you can set minOccurrence and maxOccurrence. That way you can assert on the size of the collection.

和示例:

Contract contractDsl = Contract.make {
request {
    method 'GET'
    urlPath '/get'
    body([
            duck: 123,
            alpha: "abc",
            number: 123,
            aBoolean: true,
            date: "2017-01-01",
            dateTime: "2017-01-01T01:23:45",
            time: "01:02:34",
            valueWithoutAMatcher: "foo",
            valueWithTypeMatch: "string"
    ])
    stubMatchers {
        jsonPath('$.duck', byRegex("[0-9]{3}"))
        jsonPath('$.duck', byEquality())
        jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
        jsonPath('$.alpha', byEquality())
        jsonPath('$.number', byRegex(number()))
        jsonPath('$.aBoolean', byRegex(anyBoolean()))
        jsonPath('$.date', byDate())
        jsonPath('$.dateTime', byTimestamp())
        jsonPath('$.time', byTime())
    }
    headers {
        contentType(applicationJson())
    }
}
response {
    status 200
    body([
            duck: 123,
            alpha: "abc",
            number: 123,
            aBoolean: true,
            date: "2017-01-01",
            dateTime: "2017-01-01T01:23:45",
            time: "01:02:34",
            valueWithoutAMatcher: "foo",
            valueWithTypeMatch: "string",
            valueWithMin: [
                1,2,3
            ],
            valueWithMax: [
                1,2,3
            ],
            valueWithMinMax: [
                1,2,3
            ],
    ])
    testMatchers {
        // asserts the jsonpath value against manual regex
        jsonPath('$.duck', byRegex("[0-9]{3}"))
        // asserts the jsonpath value against the provided value
        jsonPath('$.duck', byEquality())
        // asserts the jsonpath value against some default regex
        jsonPath('$.alpha', byRegex(onlyAlphaUnicode()))
        jsonPath('$.alpha', byEquality())
        jsonPath('$.number', byRegex(number()))
        jsonPath('$.aBoolean', byRegex(anyBoolean()))
        // asserts vs inbuilt time related regex
        jsonPath('$.date', byDate())
        jsonPath('$.dateTime', byTimestamp())
        jsonPath('$.time', byTime())
        // asserts that the resulting type is the same as in response body
        jsonPath('$.valueWithTypeMatch', byType())
        jsonPath('$.valueWithMin', byType {
            // results in verification of size of array (min 1)
            minOccurrence(1)
        })
        jsonPath('$.valueWithMax', byType {
            // results in verification of size of array (max 3)
            maxOccurrence(3)
        })
        jsonPath('$.valueWithMinMax', byType {
            // results in verification of size of array (min 1 & max 3)
            minOccurrence(1)
            maxOccurrence(3)
        })
    }
    headers {
        contentType(applicationJson())
    }
}
}

和生成的测试示例(断言大小的部分)

assertThat((Object) parsedJson.read("$.valueWithMin")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMin", java.util.Collection.class).size()).isGreaterThanOrEqualTo(1);
assertThat((Object) parsedJson.read("$.valueWithMax")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMax", java.util.Collection.class).size()).isLessThanOrEqualTo(3);
assertThat((Object) parsedJson.read("$.valueWithMinMax")).isInstanceOf(java.util.List.class);
assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3);