Define contract for possible empty array?

6.1k views Asked by At

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?

1

There are 1 answers

4
Marcin Grzejszczak On BEST ANSWER

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