Search code examples
spring-cloudspring-cloud-contract

Define contract for possible empty array?


I'm trying to define a CDC contract using Spring-Cloud-Contract like this:

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_\- ]*/))}"
                    }
                ]
            }]
        """
    }
}

In the generated tests, this results in the following assertions:

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_\\- ]*");

But in my tests I want to allow that the sections array is empty, like the first example. Now, if my test implementation returns an empty sections array, the generated tests fail because it cannot find the sections' id for an empty array.

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

I also tried with optional(), but the only difference is that the regex includes a '?' at the end. The JSON assertion still fails.

In the stubs, both results are returned, but for the test, I want the test to succeed for both, too. Are the test assertions purely generated on the last occurence of each attribute? Is there no possibility to have something like 'optional()' on the array?


Solution

  • It wasn't possible to do additional checks like this up till version 1.0.3.RELEASE. Since that version you can provide additional matchers - http://cloud.spring.io/spring-cloud-static/spring-cloud-contract/1.0.3.RELEASE/#_dynamic_properties_in_matchers_sections . You can match byType with additional check related to size.

    Taken from the docs:

    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.

    And example:

    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())
        }
    }
    }
    

    and example of a generated test (part for asserting sizes)

    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);