Adding an option to validate children nodes in an array (#219)

without this change it's impossible to assert with response matchers all elements of an array
with this change we're using AssertJs conditions in case the pattern contains [*]

fixes #217
This commit is contained in:
Marcin Grzejszczak
2017-02-08 11:41:59 +01:00
committed by GitHub
parent 8449382110
commit ab7fe13aac
7 changed files with 154 additions and 10 deletions

View File

@@ -342,7 +342,7 @@ assertions and the one from matchers with an `and` section):
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);
assertThat(parsedJson.read("$.valueWithMinMax", java.util.Collection.class).size()).isBetween(1, 3);
----
and the WireMock stub like this:

View File

@@ -39,4 +39,11 @@ enum MatchingType {
* provided regex
*/
REGEX
static boolean regexRelated(MatchingType type) {
if (type == EQUALITY || type == TYPE ) {
return false
}
return true
}
}

View File

@@ -0,0 +1,22 @@
package org.springframework.cloud.contract.spec.internal
import spock.lang.Specification
/**
* @author Marcin Grzejszczak
*/
class MatchingTypeSpec extends Specification {
def "should return [#expected] for type [#type]"() {
expect:
MatchingType.regexRelated(type) == expected
where:
type | expected
MatchingType.EQUALITY | false
MatchingType.TYPE | false
MatchingType.REGEX | true
MatchingType.DATE | true
MatchingType.TIME | true
MatchingType.TIMESTAMP | true
}
}

View File

@@ -90,7 +90,11 @@ class BlockBuilder {
}
BlockBuilder addAtTheEnd(String toAdd) {
if (builder.charAt(builder.length() - 1) as String == '\n') {
String lastChar = builder.charAt(builder.length() - 1) as String
String secondLastChar = builder.length() >= 2 ? builder.charAt(builder.length() - 2) as String : ""
if (endsWithNewLine(lastChar) && aSpecialSign(secondLastChar, toAdd)) {
return this
} else if (endsWithNewLine(lastChar) && !aSpecialSign(secondLastChar, toAdd)) {
builder.replace(builder.length() - 1, builder.length(), toAdd)
builder << '\n'
} else {
@@ -99,6 +103,17 @@ class BlockBuilder {
return this
}
private boolean endsWithNewLine(String character) {
return character as String == '\n'
}
private boolean aSpecialSign(String character, String toAdd) {
if (!character) {
return false
}
return character == "{" || character == toAdd
}
@Override
String toString() {
return builder.toString()

View File

@@ -313,13 +313,17 @@ abstract class MethodBodyBuilder {
// for the rest we'll do JsonPath matching in brute force
bodyMatchers.jsonPathMatchers().each {
if (it.value() || it.matchingType() == MatchingType.EQUALITY) {
String comparisonMethod = it.matchingType() == MatchingType.EQUALITY ? "isEqualTo" : "matches"
String path = quotedAndEscaped(it.path())
Object retrievedValue = value(copiedBody, it)
String valueAsParam = retrievedValue instanceof String ? quotedAndEscaped(retrievedValue.toString()) : retrievedValue.toString()
String classToCastTo = "${retrievedValue.class.simpleName}.class"
String path = quotedAndEscaped(it.path())
String method = "assertThat(parsedJson.read(${path}, ${classToCastTo})).${comparisonMethod}(${valueAsParam})"
bb.addLine(postProcessJsonPathCall(method))
if (path.contains("[*]") && MatchingType.regexRelated(it.matchingType())) {
buildCustomMatchingConditionForEachElement(bb, path, valueAsParam)
} else {
String comparisonMethod = it.matchingType() == MatchingType.EQUALITY ? "isEqualTo" : "matches"
String classToCastTo = "${retrievedValue.class.simpleName}.class"
String method = "assertThat(parsedJson.read(${path}, ${classToCastTo})).${comparisonMethod}(${valueAsParam})"
bb.addLine(postProcessJsonPathCall(method))
}
addColonIfRequired(bb)
} else {
Object elementFromBody = value(copiedBody, it)
@@ -337,6 +341,23 @@ abstract class MethodBodyBuilder {
processBodyElement(bb, "", convertedResponseBody)
}
protected void buildCustomMatchingConditionForEachElement(BlockBuilder bb, String path, String valueAsParam) {
String method = "assertThat(parsedJson.read(${path}, java.util.Collection.class)).as(\"All elements match regex\").are("
String newCondition = "new org.assertj.core.api.Condition<Object>() {"
String overriddenMethod = "@Override public boolean matches(Object o) {"
String matches = "return ((String)o).matches(${valueAsParam})"
String methodEnd = "}"
String classEnd = "})"
bb.addLine(postProcessJsonPathCall(method))
bb.startBlock().startBlock().addLine(newCondition)
bb.startBlock().addLine(overriddenMethod)
bb.startBlock().addLine(postProcessJsonPathCall(matches))
addColonIfRequired(bb)
bb.endBlock().addLine(methodEnd)
bb.endBlock().addLine(classEnd)
bb.endBlock().endBlock()
}
protected Object value(def body, BodyMatcher bodyMatcher) {
if (bodyMatcher.matchingType() == MatchingType.EQUALITY || !bodyMatcher.value()) {
return retrieveObjectByPath(body, bodyMatcher.path())
@@ -374,7 +395,7 @@ abstract class MethodBodyBuilder {
protected String sizeCheckMethod(BodyMatcher bodyMatcher) {
if (bodyMatcher.minTypeOccurrence() != null && bodyMatcher.maxTypeOccurrence() != null) {
return "isStrictlyBetween(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})"
return "isBetween(${bodyMatcher.minTypeOccurrence()}, ${bodyMatcher.maxTypeOccurrence()})"
} else if (bodyMatcher.minTypeOccurrence() != null ) {
return "isGreaterThanOrEqualTo(${bodyMatcher.minTypeOccurrence()})"
} else if (bodyMatcher.maxTypeOccurrence() != null) {

View File

@@ -74,7 +74,13 @@ class JsonToJsonPathsConverter {
DocumentContext context = JsonPath.parse(jsonCopy)
if (bodyMatchers?.hasMatchers()) {
bodyMatchers.jsonPathMatchers().each { BodyMatcher matcher ->
context.delete(matcher.path())
try {
context.delete(matcher.path())
} catch (RuntimeException e) {
if (log.isDebugEnabled()) {
log.debug("Exception occurred while trying to delete path [${matcher.path()}]", e)
}
}
}
}
return jsonCopy

View File

@@ -153,7 +153,7 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
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((Object) parsedJson.read("' + rootElement + '.valueWithMinMax")).isInstanceOf(java.util.List.class)')
test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class).size()).isStrictlyBetween(1, 3)')
test.contains('assertThat(parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class).size()).isBetween(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((Object) parsedJson.read("' + rootElement + '.valueWithMaxEmpty")).isInstanceOf(java.util.List.class)')
@@ -169,4 +169,77 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
"JaxRsClientJUnitMethodBodyBuilder" | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
}
@Issue('#217')
def "should allow complex matchers for [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url 'person'
}
response {
status 200
body([
"firstName": "Jane",
"lastName": "Doe",
"isAlive": true,
"address": [
"postalCode": "98101",
],
"phoneNumbers": [
[
"type": "home",
"number": "999 999-9999",
]
],
"gender": [
"type": "female",
],
"children": [
[
"firstName": "Kid",
"age": 55,
]
],
])
testMatchers {
jsonPath('$.phoneNumbers', byType {
minOccurrence(0) // min occurrence of 1
maxOccurrence(4) // max occurrence of 3
})
jsonPath('$.phoneNumbers[*].number', byRegex("^[0-9]{3} [0-9]{3}-[0-9]{4}\$"))
jsonPath('$..number', byRegex("^[0-9]{3} [0-9]{3}-[0-9]{4}\$"))
}
headers {
contentType('application/json')
}
}
}
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)).as("All elements match regex").are(')
test.contains('new org.assertj.core.api.Condition<Object>() {')
test.contains('@Override public boolean matches(Object o) {')
test.contains('return ((String)o).matches("^[0-9]{3} [0-9]{3}-[0-9]{4}' + rootElement + '")')
test.contains('assertThat(parsedJson.read("' + rootElement + '..number", String.class)).matches("^[0-9]{3} [0-9]{3}-[0-9]{4}' + rootElement + '")')
!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) } | '$'
}
}