From 8b91361bc132377424385b4611b2fe56523e0c7a Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Thu, 9 Feb 2017 16:13:28 +0100 Subject: [PATCH] Added flattened size assertion and refactored collection assertions with this change we no longer call the integer assertions on the size. We have our own custom size assertions. Also we can do a flattened size assertion. If in the jsonpath there is [*] we can finally check the whole size and assert it properly related to #217 --- .../verifier/builder/MethodBodyBuilder.groovy | 17 +- .../verifier/assertion/CollectionAssert.java | 112 +++++++++ .../assertion/CollectionAssertTests.java | 219 ++++++++++++++++++ ...vcMethodBodyBuilderWithMatchersSpec.groovy | 64 ++++- 4 files changed, 403 insertions(+), 9 deletions(-) diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy index b01af2283f..a27acccea1 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/MethodBodyBuilder.groovy @@ -329,7 +329,7 @@ abstract class MethodBodyBuilder { Object elementFromBody = value(copiedBody, it) if (it.minTypeOccurrence() != null || it.maxTypeOccurrence() != null) { checkType(bb, it, elementFromBody) - String method = "assertThat(parsedJson.read(${quotedAndEscaped(it.path())}, java.util.Collection.class).size()).${sizeCheckMethod(it)}" + String method = "assertThat(parsedJson.read(${quotedAndEscaped(it.path())}, java.util.Collection.class)).${sizeCheckMethod(it)}" bb.addLine(postProcessJsonPathCall(method)) addColonIfRequired(bb) } else { @@ -382,15 +382,24 @@ abstract class MethodBodyBuilder { } protected String sizeCheckMethod(BodyMatcher bodyMatcher) { + String prefix = sizeCheckPrefix(bodyMatcher) if (bodyMatcher.minTypeOccurrence() != null && bodyMatcher.maxTypeOccurrence() != null) { - return "isBetween(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})" + return "${prefix}Between(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})" } else if (bodyMatcher.minTypeOccurrence() != null ) { - return "isGreaterThanOrEqualTo(${bodyMatcher.minTypeOccurrence()})" + return "${prefix}GreaterThanOrEqualTo(${bodyMatcher.minTypeOccurrence()})" } else if (bodyMatcher.maxTypeOccurrence() != null) { - return "isLessThanOrEqualTo(${bodyMatcher.maxTypeOccurrence()})" + return "${prefix}LessThanOrEqualTo(${bodyMatcher.maxTypeOccurrence()})" } } + private String sizeCheckPrefix(BodyMatcher bodyMatcher) { + String prefix = "has" + if (bodyMatcher.path().contains("[*]")) { + prefix = prefix + "Flattened" + } + return prefix + "Size" + } + protected String quotedAndEscaped(String string) { return '"' + StringEscapeUtils.escapeJava(string) + '"' } diff --git a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/CollectionAssert.java b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/CollectionAssert.java index 013ad19494..5d6b746318 100644 --- a/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/CollectionAssert.java +++ b/spring-cloud-contract-verifier/src/main/java/org/springframework/cloud/contract/verifier/assertion/CollectionAssert.java @@ -1,6 +1,8 @@ package org.springframework.cloud.contract.verifier.assertion; +import java.util.Collection; import java.util.Iterator; +import java.util.Map; import org.assertj.core.api.IterableAssert; @@ -35,4 +37,114 @@ public class CollectionAssert extends IterableAssert { } return this; } + + /** + * Flattens the collection and checks whether size is greater than or equal to the provided value + * @param size - the flattened collection should have size greater than or equal to this value + * @return this + */ + public CollectionAssert hasFlattenedSizeGreaterThanOrEqualTo(int size) { + isNotNull(); + int flattenedSize = flattenedSize(0, this.actual); + if (!(flattenedSize >= size)) { + failWithMessage("The flattened size <%s> is not greater or equal to <%s>", flattenedSize, size); + } + return this; + } + + /** + * Flattens the collection and checks whether size is less than or equal to the provided value + * @param size - the flattened collection should have size less than or equal to this value + * @return this + */ + public CollectionAssert hasFlattenedSizeLessThanOrEqualTo(int size) { + isNotNull(); + int flattenedSize = flattenedSize(0, this.actual); + if (!(flattenedSize <= size)) { + failWithMessage("The flattened size <%s> is not less or equal to <%s>", flattenedSize, size); + } + return this; + } + + /** + * Flattens the collection and checks whether size is between the provided value + * @param lowerBound - the flattened collection should have size greater than or equal to this value + * @param higherBound - the flattened collection should have size less than or equal to this value + * @return this + */ + public CollectionAssert hasFlattenedSizeBetween(int lowerBound, int higherBound) { + isNotNull(); + int flattenedSize = flattenedSize(0, this.actual); + if (!(flattenedSize >= lowerBound && flattenedSize <= higherBound)) { + failWithMessage("The flattened size <%s> is not between <%s> and <%s>", flattenedSize, lowerBound, higherBound); + } + return this; + } + + /** + * Checks whether size is greater than or equal to the provided value + * @param size - the collection should have size greater than or equal to this value + * @return this + */ + public CollectionAssert hasSizeGreaterThanOrEqualTo(int size) { + isNotNull(); + int actualSize = size(this.actual); + if (!(actualSize >= size)) { + failWithMessage("The size <%s> is not greater or equal to <%s>", actualSize, size); + } + return this; + } + + /** + * Checks whether size is less than or equal to the provided value + * @param size - the collection should have size less than or equal to this value + * @return this + */ + public CollectionAssert hasSizeLessThanOrEqualTo(int size) { + isNotNull(); + int actualSize = size(this.actual); + if (!(actualSize <= size)) { + failWithMessage("The size <%s> is not less or equal to <%s>", actualSize, size); + } + return this; + } + + /** + * Checks whether size is between the provided value + * @param lowerBound - the collection should have size greater than or equal to this value + * @param higherBound - the collection should have size less than or equal to this value + * @return this + */ + public CollectionAssert hasSizeBetween(int lowerBound, int higherBound) { + isNotNull(); + int size = size(this.actual); + if (!(size >= lowerBound && size <= higherBound)) { + failWithMessage("The size <%s> is not between <%s> and <%s>", size, lowerBound, higherBound); + } + return this; + } + + private int flattenedSize(int counter, Object object) { + if (object instanceof Map) { + return counter + ((Map) object).size(); + } else if (object instanceof Iterator) { + Iterator iterator = ((Iterator) object); + while (iterator.hasNext()) { + Object next = iterator.next(); + counter = flattenedSize(counter, next); + } + return counter; + } else if (object instanceof Collection) { + return flattenedSize(counter, ((Collection) object).iterator()); + } + return counter; + } + + private int size(Iterable iterable) { + int size = 0; + for (Object value : iterable) { + size++; + } + return size; + } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertTests.java b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertTests.java index f8854e232a..ef6b74ad09 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertTests.java +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/assertion/CollectionAssertTests.java @@ -2,7 +2,9 @@ package org.springframework.cloud.contract.verifier.assertion; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.assertj.core.api.Assertions; import org.junit.Test; @@ -55,6 +57,206 @@ public class CollectionAssertTests { } } + @Test + public void should_not_throw_an_exception_when_flattened_size_is_greater_than_or_equal_to_provided_size() { + Collection collection = nestedCollection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasFlattenedSizeGreaterThanOrEqualTo(0) + .hasFlattenedSizeGreaterThanOrEqualTo(4); + } + + @Test + public void should_throw_an_exception_when_flattened_size_is_not_greater_than_or_equal_to_provided_size() { + Collection collection = nestedCollection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeGreaterThanOrEqualTo(5); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The flattened size <4> is not greater or equal to <5>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_flattened_greater_than_or_equal() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeGreaterThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + @Test + public void should_not_throw_an_exception_when_flattened_size_is_less_than_or_equal_to_provided_size() { + Collection collection = nestedCollection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasFlattenedSizeLessThanOrEqualTo(5) + .hasFlattenedSizeLessThanOrEqualTo(4); + } + + @Test + public void should_throw_an_exception_when_flattened_size_is_not_less_than_or_equal_to_provided_size() { + Collection collection = nestedCollection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeLessThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The flattened size <4> is not less or equal to <1>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_flattened_less_than_or_equal() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeLessThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + + @Test + public void should_not_throw_an_exception_when_flattened_size_is_between_the_provided_sizes() { + Collection collection = nestedCollection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasFlattenedSizeBetween(1, 5) + .hasFlattenedSizeBetween(4, 4); + } + + @Test + public void should_throw_an_exception_when_flattened_size_is_not_between_the_provided_sizes() { + Collection collection = nestedCollection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeBetween(5, 7); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The flattened size <4> is not between <5> and <7>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_flattened_between() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasFlattenedSizeBetween(1, 2); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + @Test + public void should_not_throw_an_exception_when_size_is_greater_than_or_equal_to_provided_size() { + Collection collection = collection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasSizeGreaterThanOrEqualTo(0) + .hasSizeGreaterThanOrEqualTo(3); + } + + @Test + public void should_throw_an_exception_when_size_is_not_greater_than_or_equal_to_provided_size() { + Collection collection = collection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeGreaterThanOrEqualTo(5); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The size <3> is not greater or equal to <5>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_greater_than_or_equal() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeGreaterThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + @Test + public void should_not_throw_an_exception_when_size_is_less_than_or_equal_to_provided_size() { + Collection collection = collection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasSizeLessThanOrEqualTo(4) + .hasSizeLessThanOrEqualTo(3); + } + + @Test + public void should_throw_an_exception_when_size_is_not_less_than_or_equal_to_provided_size() { + Collection collection = collection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeLessThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The size <3> is not less or equal to <1>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_less_than_or_equal() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeLessThanOrEqualTo(1); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + + + @Test + public void should_not_throw_an_exception_when_size_is_between_the_provided_sizes() { + Collection collection = collection(); + + SpringCloudContractAssertions.assertThat(collection) + .hasSizeBetween(1, 4) + .hasSizeBetween(3, 3); + } + + @Test + public void should_throw_an_exception_when_size_is_not_between_the_provided_sizes() { + Collection collection = collection(); + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeBetween(5, 7); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("The size <3> is not between <5> and <7>"); + } + } + + @Test + public void should_throw_an_exception_when_collection_is_null_for_between() { + Collection collection = null; + + try { + SpringCloudContractAssertions.assertThat(collection).hasSizeBetween(1, 2); + Assertions.fail("should throw exception"); + } catch (AssertionError e) { + Assertions.assertThat(e).hasMessageContaining("Expecting actual not to be null"); + } + } + private Collection collection() { List list = new ArrayList<>(); list.add("a"); @@ -63,4 +265,21 @@ public class CollectionAssertTests { return list; } + private Collection nestedCollection() { + List list = new ArrayList<>(); + List list1 = new ArrayList<>(); + Map map1 = new HashMap<>(); + map1.put("a", "1"); + map1.put("b", "2"); + map1.put("c", "3"); + List list2 = new ArrayList<>(); + Map map2 = new HashMap<>(); + map2.put("d", "4"); + list.add(list1); + list.add(list2); + list1.add(map1); + list2.add(map2); + return list; + } + } \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy index 800a53b98c..4bf74dc0e4 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MockMvcMethodBodyBuilderWithMatchersSpec.groovy @@ -149,15 +149,15 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements test.contains('assertThat(parsedJson.read("' + rootElement + '.time", String.class)).matches("(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])")') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithTypeMatch")).isInstanceOf(java.lang.String.class)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMin")).isInstanceOf(java.util.List.class)') - test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMin", java.util.Collection.class).size()).isGreaterThanOrEqualTo(1)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMin", java.util.Collection.class)).hasSizeGreaterThanOrEqualTo(1)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMax")).isInstanceOf(java.util.List.class)') - test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMax", java.util.Collection.class).size()).isLessThanOrEqualTo(3)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMax", java.util.Collection.class)).hasSizeLessThanOrEqualTo(3)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMinMax")).isInstanceOf(java.util.List.class)') - test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class).size()).isBetween(1, 3)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class)).hasSizeBetween(1, 3)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMinEmpty")).isInstanceOf(java.util.List.class)') - test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinEmpty", java.util.Collection.class).size()).isGreaterThanOrEqualTo(0)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinEmpty", java.util.Collection.class)).hasSizeGreaterThanOrEqualTo(0)') test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMaxEmpty")).isInstanceOf(java.util.List.class)') - test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMaxEmpty", java.util.Collection.class).size()).isLessThanOrEqualTo(0)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMaxEmpty", java.util.Collection.class)).hasSizeLessThanOrEqualTo(0)') !test.contains('cursor') and: SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, blockBuilder.toString()) @@ -238,4 +238,58 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$' } + + @Issue('#217') + def "should use the flattened assertions when jsonpath contains [*] for [#methodBuilderName]"() { + given: + Contract contractDsl = Contract.make { + request { + method 'GET' + url 'person' + } + response { + status 200 + body([ + "phoneNumbers": [ + number: "foo" + ] + ]) + testMatchers { + jsonPath('$.phoneNumbers[*].number', byType { + minOccurrence(0) + maxOccurrence(4) + }) + jsonPath('$.phoneNumbers[*].number', byType { + minOccurrence(0) + }) + jsonPath('$.phoneNumbers[*].number', byType { + maxOccurrence(4) + }) + } + } + } + MethodBodyBuilder builder = methodBuilder(contractDsl) + BlockBuilder blockBuilder = new BlockBuilder(" ") + when: + builder.appendTo(blockBuilder) + def test = blockBuilder.toString() + then: + test.contains('assertThat(parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).hasFlattenedSizeBetween(0, 4)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).hasFlattenedSizeGreaterThanOrEqualTo(0)') + test.contains('assertThat(parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).hasFlattenedSizeLessThanOrEqualTo(4)') + !test.contains('cursor') + and: + try { + SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, blockBuilder.toString()) + } catch(NoClassDefFoundError error) { + // that's actually expected since we're creating an anonymous class + } + where: + methodBuilderName | methodBuilder | rootElement + "MockMvcSpockMethodBuilder" | { Contract dsl -> new MockMvcSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$' + "MockMvcJUnitMethodBuilder" | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '$' + "JaxRsClientSpockMethodRequestProcessingBodyBuilder" | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$' + "JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$' + } + }