Webtestclient support (#757)

* Fix incorrect spring cloud dependencies versioning.
* Test fix and minor refactoring.
* Add docs.
* Fixes after code review.

fixes gh-422
This commit is contained in:
Olga Maciaszek-Sharma
2018-10-22 14:34:03 +02:00
committed by Marcin Grzejszczak
parent fea5c743ce
commit cf21f18938
20 changed files with 5408 additions and 5123 deletions

View File

@@ -180,8 +180,7 @@ include::{wiremock_tests}/src/test/java/org/springframework/cloud/contract/wirem
https://projects.spring.io/spring-restdocs[Spring REST Docs] can be used to generate
documentation (for example in Asciidoctor format) for an HTTP API with Spring MockMvc
or `WebTestClient` or
Rest Assured. At the same time that you generate documentation for your API, you can also
or `WebTestClient` or Rest Assured. At the same time that you generate documentation for your API, you can also
generate WireMock stubs by using Spring Cloud Contract WireMock. To do so, write your
normal REST Docs test cases and use `@AutoConfigureRestDocs` to have stubs be
automatically generated in the REST Docs output directory. The following code shows an

View File

@@ -363,10 +363,38 @@ public void validate_shouldMarkClientAsFraud() throws Exception {
----
The preceding example uses Spring's `MockMvc` to run the tests. This is the default test
mode for HTTP contracts. However, JAX-RX client and explicit HTTP invocations can also be
mode for HTTP contracts. However, JAX-RS client and explicit HTTP invocations can also be
used. (To do so, change the `testMode` property of the plugin to `JAX-RS` or `EXPLICIT`,
respectively.)
Since 2.1.0, it is also possible to use `RestAssuredWebTestClient`with Spring's reactive `WebTestClient`
run under the hood. This is particularly recommended while working with Reactive, `Web-Flux`-based applications.
In order to use `WebTestClient` set `testMode` to `WEBTESTCLIENT`.
Here is an example of a test generated in `WEBTESTCLIENT` test mode:
[source,java,indent=0]
----
@Test
public void validate_shouldRejectABeerIfTooYoung() throws Exception {
// given:
WebTestClientRequestSpecification request = given()
.header("Content-Type", "application/json")
.body("{\"age\":10}");
// when:
WebTestClientResponse response = given().spec(request)
.post("/check");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).matches("application/json.*");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).field("['status']").isEqualTo("NOT_OK");
}
----
Apart from the default JUnit 4, you can instead use JUnit 5 or Spock tests, by setting the plugin
`testFramework` property to either `JUNIT5` or `Spock`.

View File

@@ -217,7 +217,7 @@ contracts {
==== Configuration Options
* *testMode*: Defines the mode for acceptance tests. By default, the mode is MockMvc,
which is based on Spring's MockMvc. It can also be changed to *JaxRsClient* or to
which is based on Spring's MockMvc. It can also be changed to *WebTestClient*, *JaxRsClient* or to
*Explicit* for real HTTP calls.
* *imports*: Creates an array with imports that should be included in generated tests
(for example ['org.myorg.Matchers']). By default, it creates an empty array.
@@ -569,7 +569,7 @@ definition or the `execution` definition, as shown here:
==== Configuration Options
* *testMode*: Defines the mode for acceptance tests. By default, the mode is MockMvc,
which is based on Spring's MockMvc. It can also be changed to *JaxRsClient* or to
which is based on Spring's MockMvc. It can also be changed to *WebTestClient*, *JaxRsClient* or to
*Explicit* for real HTTP calls.
* *basePackageForTests*: Specifies the base package for all generated tests. If not set,
the value is picked from `baseClassForTests`'s package and from `packageWithBaseClasses`.

View File

@@ -17,7 +17,7 @@
<wiremock.version>2.19.0</wiremock.version>
<jsonassert.version>0.4.13</jsonassert.version>
<aether.version>1.0.2.v20150114</aether.version>
<rest-assured.version>3.0.7</rest-assured.version>
<rest-assured.version>3.2.0</rest-assured.version>
</properties>
<dependencyManagement>
<dependencies>
@@ -115,6 +115,21 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-web-test-client</artifactId>
<version>${rest-assured.version}</version>
<exclusions>
<exclusion>
<artifactId>spring-context</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
<exclusion>
<artifactId>spring-webflux</artifactId>
<groupId>org.springframework</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>

View File

@@ -133,6 +133,16 @@
<artifactId>spring-mock-mvc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>spring-web-test-client</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webflux</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>

View File

@@ -34,9 +34,9 @@ import java.util.regex.Pattern
*/
@PackageScope
@TypeChecked
class MockMvcSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestProcessingBodyBuilder {
class HttpSpockMethodRequestProcessingBodyBuilder extends SpockMethodRequestProcessingBodyBuilder {
MockMvcSpockMethodRequestProcessingBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) {
HttpSpockMethodRequestProcessingBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) {
super(stubDefinition, configProperties)
}

View File

@@ -117,6 +117,7 @@ class JavaTestGenerator implements SingleTestGenerator {
clazz.addStaticImports(httpImportProvider.getStaticImports(configProperties.targetFramework, configProperties.testMode))
}
// TODO for 2.2: leave only RestAssured 3
private String getRestAssuredPackage() {
boolean restAssured2Present = this.checker.isClassPresent(REST_ASSURED_2_0_CLASS)
String restAssuredPackage = restAssured2Present ? 'com.jayway.restassured' : 'io.restassured'

View File

@@ -19,7 +19,6 @@ package org.springframework.cloud.contract.verifier.builder
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import groovy.util.logging.Commons
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import org.springframework.cloud.contract.verifier.config.TestMode
@@ -118,14 +117,19 @@ class MethodBuilder {
return new JaxRsClientJUnitMethodBodyBuilder(stubContent, configProperties)
}
return new JaxRsClientSpockMethodRequestProcessingBodyBuilder(stubContent, configProperties)
} else if (configProperties.testMode == TestMode.WEBTESTCLIENT) {
if (isJUnitType()) {
return new WebTestClientJUnitMethodBodyBuilder(stubContent, configProperties)
}
return new HttpSpockMethodRequestProcessingBodyBuilder(stubContent, configProperties)
} else if (configProperties.testMode == TestMode.EXPLICIT) {
if (isJUnitType()) {
return new ExplicitJUnitMethodBodyBuilder(stubContent, configProperties)
}
// in Groovy we're using def so we don't have to update the imports
return new MockMvcSpockMethodRequestProcessingBodyBuilder(stubContent, configProperties)
return new HttpSpockMethodRequestProcessingBodyBuilder(stubContent, configProperties)
} else if (configProperties.targetFramework == SPOCK) {
return new MockMvcSpockMethodRequestProcessingBodyBuilder(stubContent, configProperties)
return new HttpSpockMethodRequestProcessingBodyBuilder(stubContent, configProperties)
}
return new MockMvcJUnitMethodBodyBuilder(stubContent, configProperties)
}

View File

@@ -16,8 +16,8 @@
package org.springframework.cloud.contract.verifier.builder
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import groovy.transform.TypeChecked
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
@@ -28,7 +28,7 @@ import org.springframework.cloud.contract.verifier.config.ContractVerifierConfig
*
* @since 1.0.0
*/
@TypeChecked
@CompileStatic
@PackageScope
class MockMvcJUnitMethodBodyBuilder extends RestAssuredJUnitMethodBodyBuilder {
@@ -38,12 +38,12 @@ class MockMvcJUnitMethodBodyBuilder extends RestAssuredJUnitMethodBodyBuilder {
@Override
protected String returnedResponseType() {
return "ResponseOptions"
return 'ResponseOptions'
}
@Override
protected String returnedRequestType() {
return "MockMvcRequestSpecification"
return 'MockMvcRequestSpecification'
}
}

View File

@@ -0,0 +1,45 @@
package org.springframework.cloud.contract.verifier.builder
import groovy.transform.CompileStatic
import groovy.transform.PackageScope
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.spec.internal.Url
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
/**
* A {@link JUnitMethodBodyBuilder} implementation that uses WebTestClient to send requests.
*
* @author Olga Maciaszek-Sharma
*
* @since 2.1.0
*/
@CompileStatic
@PackageScope
class WebTestClientJUnitMethodBodyBuilder extends RestAssuredJUnitMethodBodyBuilder {
WebTestClientJUnitMethodBodyBuilder(Contract stubDefinition, ContractVerifierConfigProperties configProperties) {
super(stubDefinition, configProperties)
}
@Override
protected String returnedResponseType() {
return 'WebTestClientResponse'
}
@Override
protected String returnedRequestType() {
return 'WebTestClientRequestSpecification'
}
@Override
protected void when(BlockBuilder bb) {
bb.addLine(getInputString(request))
bb.indent()
Url url = getUrl(request)
addQueryParameters(url, bb)
addUrl(url, bb)
addColonIfRequired(bb)
bb.unindent()
}
}

View File

@@ -10,6 +10,7 @@ import static org.springframework.cloud.contract.verifier.config.TestFramework.S
import static org.springframework.cloud.contract.verifier.config.TestMode.EXPLICIT
import static org.springframework.cloud.contract.verifier.config.TestMode.JAXRSCLIENT
import static org.springframework.cloud.contract.verifier.config.TestMode.MOCKMVC
import static org.springframework.cloud.contract.verifier.config.TestMode.WEBTESTCLIENT
/**
* Provides imports based on test framework and test mode.
@@ -21,29 +22,38 @@ import static org.springframework.cloud.contract.verifier.config.TestMode.MOCKMV
class HttpImportProvider {
private final Map<TestMode, ImportDefinitions> TEST_MODE_SPECIFIC_IMPORTS = [
(JAXRSCLIENT): new ImportDefinitions([], ['javax.ws.rs.client.Entity.*']),
(MOCKMVC) : new ImportDefinitions([], ["${restAssuredPackage}.module.mockmvc.RestAssuredMockMvc.*"]),
(EXPLICIT) : new ImportDefinitions([], ["${restAssuredPackage}.RestAssured.*"])]
(JAXRSCLIENT) : new ImportDefinitions([], ['javax.ws.rs.client.Entity.*']),
(MOCKMVC) : new ImportDefinitions([], ["${restAssuredPackage}.module.mockmvc.RestAssuredMockMvc.*"]),
(EXPLICIT) : new ImportDefinitions([], ["${restAssuredPackage}.RestAssured.*"]),
(WEBTESTCLIENT): new ImportDefinitions([], ['io.restassured.module.webtestclient.RestAssuredWebTestClient.*'])]
private final Map<Tuple2<TestFramework, TestMode>, ImportDefinitions> FRAMEWORK_AND_TEST_MODE_SPECIFIC_IMPORTS = [
(new Tuple2(JUNIT, JAXRSCLIENT)) : new ImportDefinitions(['javax.ws.rs.core.Response']),
(new Tuple2(JUNIT5, JAXRSCLIENT)): new ImportDefinitions(['javax.ws.rs.core.Response']),
(new Tuple2(JUNIT, MOCKMVC)) : new ImportDefinitions([
(new Tuple2(JUNIT, JAXRSCLIENT)) : new ImportDefinitions(['javax.ws.rs.core.Response']),
(new Tuple2(JUNIT5, JAXRSCLIENT)) : new ImportDefinitions(['javax.ws.rs.core.Response']),
(new Tuple2(JUNIT, MOCKMVC)) : new ImportDefinitions([
"${restAssuredPackage}.module.mockmvc.specification.MockMvcRequestSpecification",
"${restAssuredPackage}.response.ResponseOptions"]),
(new Tuple2(JUNIT5, MOCKMVC)) : new ImportDefinitions([
(new Tuple2(JUNIT, WEBTESTCLIENT)) : new ImportDefinitions([
'io.restassured.module.webtestclient.specification.WebTestClientRequestSpecification',
'io.restassured.module.webtestclient.response.WebTestClientResponse']),
(new Tuple2(JUNIT5, MOCKMVC)) : new ImportDefinitions([
"${restAssuredPackage}.module.mockmvc.specification.MockMvcRequestSpecification",
"${restAssuredPackage}.response.ResponseOptions"]),
(new Tuple2(JUNIT, EXPLICIT)) : new ImportDefinitions(["${restAssuredPackage}.specification.RequestSpecification",
(new Tuple2(JUNIT5, WEBTESTCLIENT)): new ImportDefinitions([
'io.restassured.module.webtestclient.specification.WebTestClientRequestSpecification',
'io.restassured.module.webtestclient.response.WebTestClientResponse']),
(new Tuple2(JUNIT, EXPLICIT)) : new ImportDefinitions(["${restAssuredPackage}.specification.RequestSpecification",
"${restAssuredPackage}.response.Response"]),
(new Tuple2(JUNIT5, EXPLICIT)) : new ImportDefinitions(["${restAssuredPackage}.specification.RequestSpecification",
"${restAssuredPackage}.response.Response"]),
(new Tuple2(SPOCK, JAXRSCLIENT)) : new ImportDefinitions([]),
(new Tuple2(CUSTOM, JAXRSCLIENT)): new ImportDefinitions([]),
(new Tuple2(SPOCK, MOCKMVC)) : new ImportDefinitions([]),
(new Tuple2(CUSTOM, MOCKMVC)) : new ImportDefinitions([]),
(new Tuple2(SPOCK, EXPLICIT)) : new ImportDefinitions([]),
(new Tuple2(CUSTOM, EXPLICIT)) : new ImportDefinitions([])
(new Tuple2(JUNIT5, EXPLICIT)) : new ImportDefinitions(["${restAssuredPackage}.specification.RequestSpecification",
"${restAssuredPackage}.response.Response"]),
(new Tuple2(SPOCK, JAXRSCLIENT)) : new ImportDefinitions([]),
(new Tuple2(CUSTOM, JAXRSCLIENT)) : new ImportDefinitions([]),
(new Tuple2(SPOCK, MOCKMVC)) : new ImportDefinitions([]),
(new Tuple2(CUSTOM, MOCKMVC)) : new ImportDefinitions([]),
(new Tuple2(SPOCK, EXPLICIT)) : new ImportDefinitions([]),
(new Tuple2(CUSTOM, EXPLICIT)) : new ImportDefinitions([]),
(new Tuple2(SPOCK, WEBTESTCLIENT)) : new ImportDefinitions([]),
(new Tuple2(CUSTOM, WEBTESTCLIENT)): new ImportDefinitions([])
]
private final String restAssuredPackage

View File

@@ -37,5 +37,10 @@ enum TestMode {
/**
* Uses JAX-RS client
*/
JAXRSCLIENT
JAXRSCLIENT,
/**
* Uses Spring's reactive WebTestClient
*/
WEBTESTCLIENT
}

View File

@@ -282,7 +282,7 @@ class ContractHttpDocsSpec extends Specification {
def 'should convert dsl with optionals to proper Spock test'() {
given:
BlockBuilder blockBuilder = new BlockBuilder(" ")
new MockMvcSpockMethodRequestProcessingBodyBuilder(optionals, properties).appendTo(blockBuilder)
new HttpSpockMethodRequestProcessingBodyBuilder(optionals, properties).appendTo(blockBuilder)
expect:
String expectedTest =
// tag::optionals_test[]

View File

@@ -845,7 +845,7 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub
and:
stubMappingIsValidWireMockStub(contractDsl)
and:
SyntaxChecker.tryToCompileJava(blockBuilder.toString())
SyntaxChecker.tryToCompileJava(JaxRsClientJUnitMethodBodyBuilder.simpleName, blockBuilder.toString())
}
@Issue('#85')
@@ -974,7 +974,7 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub
then:
test.contains("assertThat(responseBody).matches(\".*\");")
and:
SyntaxChecker.tryToCompileJava(blockBuilder.toString())
SyntaxChecker.tryToCompileJava(JaxRsClientJUnitMethodBodyBuilder.simpleName, blockBuilder.toString())
}
@Issue('#150')
@@ -999,7 +999,7 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub
then:
test.contains("responseBody ==~ java.util.regex.Pattern.compile('.*')")
and:
SyntaxChecker.tryToCompileGroovy(blockBuilder.toString())
SyntaxChecker.tryToCompileGroovy(JaxRsClientJUnitMethodBodyBuilder.simpleName, blockBuilder.toString())
}
@Issue('#150')
@@ -1048,7 +1048,7 @@ class JaxRsClientMethodBuilderSpec extends Specification implements WireMockStub
test.contains("foo(responseBody)")
and:
// no static compilation due to bug in Groovy https://issues.apache.org/jira/browse/GROOVY-8055
SyntaxChecker.tryToCompileGroovy(blockBuilder.toString(), false)
SyntaxChecker.tryToCompileGroovy(JaxRsClientJUnitMethodBodyBuilder.simpleName, blockBuilder.toString(), false)
}
def "should allow c/p version of consumer producer"() {

View File

@@ -28,430 +28,438 @@ import spock.lang.Specification
class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements WireMockStubVerifier {
@Rule OutputCapture outputCapture = new OutputCapture()
@Rule
OutputCapture outputCapture = new OutputCapture()
@Shared ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(
assertJsonSize: true
)
@Shared
ContractVerifierConfigProperties properties = new ContractVerifierConfigProperties(
assertJsonSize: true
)
@Issue('#185')
def "should allow to set dynamic values via stub / test matchers for [#methodBuilderName]"() {
given:
//tag::matchers[]
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",
key: [
'complex.key' : 'foo'
]
])
bodyMatchers {
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())
jsonPath("\$.['key'].['complex.key']", byEquality())
}
headers {
contentType(applicationJson())
}
}
response {
status OK()
body([
duck: 123,
alpha: "abc",
number: 123,
positiveInteger: 1234567890,
negativeInteger: -1234567890,
positiveDecimalNumber: 123.4567890,
negativeDecimalNumber: -123.4567890,
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
],
valueWithMinEmpty: [],
valueWithMaxEmpty: [],
key: [
'complex.key' : 'foo'
],
nullValue: null
])
bodyMatchers {
// 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('$.positiveInteger', byRegex(anInteger()))
jsonPath('$.negativeInteger', byRegex(anInteger()))
jsonPath('$.positiveDecimalNumber', byRegex(aDouble()))
jsonPath('$.negativeDecimalNumber', byRegex(aDouble()))
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)
})
jsonPath('$.valueWithMinEmpty', byType {
// results in verification of size of array (min 0)
minOccurrence(0)
})
jsonPath('$.valueWithMaxEmpty', byType {
// results in verification of size of array (max 0)
maxOccurrence(0)
})
// will execute a method `assertThatValueIsANumber`
jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
jsonPath("\$.['key'].['complex.key']", byEquality())
jsonPath('$.nullValue', byNull())
}
headers {
contentType(applicationJson())
header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
}
}
}
//end::matchers[]
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
when:
builder.appendTo(blockBuilder)
def test = blockBuilder.toString()
then:
test.contains('assertThat(parsedJson.read("' + rootElement + '.duck", String.class)).matches("[0-9]{3}")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.duck", Integer.class)).isEqualTo(123)')
test.contains('assertThat(parsedJson.read("' + rootElement + '.alpha", String.class)).matches("[\\\\p{L}]*")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.alpha", String.class)).isEqualTo("abc")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.number", String.class)).matches("-?(\\\\d*\\\\.\\\\d+|\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.positiveInteger", String.class)).matches("-?(\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.negativeInteger", String.class)).matches("-?(\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.positiveDecimalNumber", String.class)).matches("-?(\\\\d*\\\\.\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.negativeDecimalNumber", String.class)).matches("-?(\\\\d*\\\\.\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.aBoolean", String.class)).matches("(true|false)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.date", String.class)).matches("(\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])")')
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((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMin", java.util.Collection.class)).as("' + rootElement + '.valueWithMin").hasSizeGreaterThanOrEqualTo(1)')
test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMax")).isInstanceOf(java.util.List.class)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMax", java.util.Collection.class)).as("' + rootElement + '.valueWithMax").hasSizeLessThanOrEqualTo(3)')
test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMinMax")).isInstanceOf(java.util.List.class)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class)).as("' + rootElement + '.valueWithMinMax").hasSizeBetween(1, 3)')
test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMinEmpty")).isInstanceOf(java.util.List.class)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMinEmpty", java.util.Collection.class)).as("' + rootElement + '.valueWithMinEmpty").hasSizeGreaterThanOrEqualTo(0)')
test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMaxEmpty")).isInstanceOf(java.util.List.class)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMaxEmpty", java.util.Collection.class)).as("' + rootElement + '.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0)')
test.contains('assertThatValueIsANumber(parsedJson.read("' + rootElement + '.duck")')
test.contains('assertThat(parsedJson.read("' + rootElement + '''.['key'].['complex.key']", String.class)).isEqualTo("foo")''')
test.contains('assertThat(parsedJson.read("' + rootElement + '.nullValue")).isNull()')
!test.contains('cursor')
and:
try {
SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, blockBuilder.toString())
} catch (ClassFormatError classFormatError) {
String output = outputCapture.toString()
assert output.contains('error: cannot find symbol')
assert output.contains('assertThatValueIsANumber(parsedJson.read("$.duck"));')
}
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) } | '$'
}
@Issue('#185')
def 'should allow to set dynamic values via stub / test matchers for [#methodBuilderName]'() {
given:
//tag::matchers[]
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',
key : [
'complex.key': 'foo'
]
])
bodyMatchers {
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())
jsonPath("\$.['key'].['complex.key']", byEquality())
}
headers {
contentType(applicationJson())
}
}
response {
status OK()
body([
duck : 123,
alpha : 'abc',
number : 123,
positiveInteger : 1234567890,
negativeInteger : -1234567890,
positiveDecimalNumber: 123.4567890,
negativeDecimalNumber: -123.4567890,
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
],
valueWithMinEmpty : [],
valueWithMaxEmpty : [],
key : [
'complex.key': 'foo'
],
nullValue : null
])
bodyMatchers {
// 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('$.positiveInteger', byRegex(anInteger()))
jsonPath('$.negativeInteger', byRegex(anInteger()))
jsonPath('$.positiveDecimalNumber', byRegex(aDouble()))
jsonPath('$.negativeDecimalNumber', byRegex(aDouble()))
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)
})
jsonPath('$.valueWithMinEmpty', byType {
// results in verification of size of array (min 0)
minOccurrence(0)
})
jsonPath('$.valueWithMaxEmpty', byType {
// results in verification of size of array (max 0)
maxOccurrence(0)
})
// will execute a method `assertThatValueIsANumber`
jsonPath('$.duck', byCommand('assertThatValueIsANumber($it)'))
jsonPath("\$.['key'].['complex.key']", byEquality())
jsonPath('$.nullValue', byNull())
}
headers {
contentType(applicationJson())
header('Some-Header', $(c('someValue'), p(regex('[a-zA-Z]{9}'))))
}
}
}
//end::matchers[]
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
when:
builder.appendTo(blockBuilder)
def test = blockBuilder.toString()
then:
test.contains('assertThat(parsedJson.read("' + rootElement + '.duck", String.class)).matches("[0-9]{3}")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.duck", Integer.class)).isEqualTo(123)')
test.contains('assertThat(parsedJson.read("' + rootElement + '.alpha", String.class)).matches("[\\\\p{L}]*")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.alpha", String.class)).isEqualTo("abc")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.number", String.class)).matches("-?(\\\\d*\\\\.\\\\d+|\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.positiveInteger", String.class)).matches("-?(\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.negativeInteger", String.class)).matches("-?(\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.positiveDecimalNumber", String.class)).matches("-?(\\\\d*\\\\.\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.negativeDecimalNumber", String.class)).matches("-?(\\\\d*\\\\.\\\\d+)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.aBoolean", String.class)).matches("(true|false)")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.date", String.class)).matches("(\\\\d\\\\d\\\\d\\\\d)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])")')
test.contains('assertThat(parsedJson.read("' + rootElement + '.dateTime", String.class)).matches("([0-9]{4})-(1[0-2]|0[1-9])-(3[01]|0[1-9]|[12][0-9])T(2[0-3]|[01][0-9]):([0-5][0-9]):([0-5][0-9])")')
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((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMin", java.util.Collection.class)).as("' + rootElement + '.valueWithMin").hasSizeGreaterThanOrEqualTo(1)')
test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMax")).isInstanceOf(java.util.List.class)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMax", java.util.Collection.class)).as("' + rootElement + '.valueWithMax").hasSizeLessThanOrEqualTo(3)')
test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMinMax")).isInstanceOf(java.util.List.class)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMinMax", java.util.Collection.class)).as("' + rootElement + '.valueWithMinMax").hasSizeBetween(1, 3)')
test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMinEmpty")).isInstanceOf(java.util.List.class)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMinEmpty", java.util.Collection.class)).as("' + rootElement + '.valueWithMinEmpty").hasSizeGreaterThanOrEqualTo(0)')
test.contains('assertThat((Object) parsedJson.read("' + rootElement + '.valueWithMaxEmpty")).isInstanceOf(java.util.List.class)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.valueWithMaxEmpty", java.util.Collection.class)).as("' + rootElement + '.valueWithMaxEmpty").hasSizeLessThanOrEqualTo(0)')
test.contains('assertThatValueIsANumber(parsedJson.read("' + rootElement + '.duck")')
test.contains('assertThat(parsedJson.read("' + rootElement + '''.['key'].['complex.key']", String.class)).isEqualTo("foo")''')
test.contains('assertThat(parsedJson.read("' + rootElement + '.nullValue")).isNull()')
!test.contains('cursor')
and:
try {
SyntaxChecker.tryToCompileWithoutCompileStatic(methodBuilderName, blockBuilder.toString())
} catch (ClassFormatError classFormatError) {
String output = outputCapture.toString()
assert output.contains('error: cannot find symbol')
assert output.contains('assertThatValueIsANumber(parsedJson.read("$.duck"));')
}
where:
methodBuilderName | methodBuilder | rootElement
HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '$'
JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
}
@Issue('#217')
def "should allow complex matchers for [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url 'person'
}
response {
status OK()
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,
]
],
])
bodyMatchers {
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((java.lang.Iterable) parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).as("' + rootElement + '.phoneNumbers[*].number").allElementsMatch("^[0-9]{3} [0-9]{3}-[0-9]{4}' + rootElement + '")')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '..number", java.util.Collection.class)).as("' + rootElement + '..number").allElementsMatch("^[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) } | '$'
}
@Issue('#217')
def 'should allow complex matchers for [#methodBuilderName]'() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url 'person'
}
response {
status OK()
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,
]
],
])
bodyMatchers {
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((java.lang.Iterable) parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).as("' + rootElement + '.phoneNumbers[*].number").allElementsMatch("^[0-9]{3} [0-9]{3}-[0-9]{4}' + rootElement + '")')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '..number", java.util.Collection.class)).as("' + rootElement + '..number").allElementsMatch("^[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
HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '$'
JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(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 OK()
body([
"phoneNumbers": [
number: "foo"
]
])
bodyMatchers {
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((java.lang.Iterable) parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).as("' + rootElement + '.phoneNumbers[*].number").hasFlattenedSizeBetween(0, 4)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).as("' + rootElement + '.phoneNumbers[*].number").hasFlattenedSizeGreaterThanOrEqualTo(0)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).as("' + rootElement + '.phoneNumbers[*].number").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) } | '$'
}
@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 OK()
body([
"phoneNumbers": [
number: 'foo'
]
])
bodyMatchers {
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((java.lang.Iterable) parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).as("' + rootElement + '.phoneNumbers[*].number").hasFlattenedSizeBetween(0, 4)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).as("' + rootElement + '.phoneNumbers[*].number").hasFlattenedSizeGreaterThanOrEqualTo(0)')
test.contains('assertThat((java.lang.Iterable) parsedJson.read("' + rootElement + '.phoneNumbers[*].number", java.util.Collection.class)).as("' + rootElement + '.phoneNumbers[*].number").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
HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '$'
JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
}
@Issue('#217')
def "should allow matcher with command to execute [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url 'person'
}
response {
status OK()
body([
"phoneNumbers": [
number: "foo"
]
])
bodyMatchers {
jsonPath('$.phoneNumbers[*].number', byCommand('foo($it)'))
}
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
when:
builder.appendTo(blockBuilder)
def test = blockBuilder.toString()
then:
test.contains('foo(parsedJson.read("' + rootElement + '.phoneNumbers[*].number")')
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) } | '$'
}
@Issue('#217')
def 'should allow matcher with command to execute [#methodBuilderName]'() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url 'person'
}
response {
status OK()
body([
"phoneNumbers": [
number: 'foo'
]
])
bodyMatchers {
jsonPath('$.phoneNumbers[*].number', byCommand('foo($it)'))
}
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
when:
builder.appendTo(blockBuilder)
def test = blockBuilder.toString()
then:
test.contains('foo(parsedJson.read("' + rootElement + '.phoneNumbers[*].number")')
where:
methodBuilderName | methodBuilder | rootElement
HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '$'
JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
}
@Issue('#217')
def "should throw an exception when command to execute references a non existing entry in the body [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url 'person'
}
response {
status OK()
body([
"phoneNumbers": [
number: "foo"
]
])
bodyMatchers {
jsonPath('$.nonExistingPhoneNumbers[*].number', byCommand('foo($it)'))
}
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
when:
builder.appendTo(blockBuilder)
then:
IllegalStateException e = thrown(IllegalStateException)
e.message.contains("Entry for the provided JSON path <\$.nonExistingPhoneNumbers[*].number> doesn't exist in the body")
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) } | '$'
}
@Issue('#217')
def 'should throw an exception when command to execute references a non existing entry in the body [#methodBuilderName]'() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url 'person'
}
response {
status OK()
body([
"phoneNumbers": [
number: 'foo'
]
])
bodyMatchers {
jsonPath('$.nonExistingPhoneNumbers[*].number', byCommand('foo($it)'))
}
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
when:
builder.appendTo(blockBuilder)
then:
IllegalStateException e = thrown(IllegalStateException)
e.message.contains('Entry for the provided JSON path <$.nonExistingPhoneNumbers[*].number> doesn\'t exist in the body')
where:
methodBuilderName | methodBuilder | rootElement
HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '$'
JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
}
@Issue("#229")
def "should work for matchers and body with json array[#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url '/api/v1/xxxx'
body(12000)
}
response {
status OK()
body ([[
[ access_token: '123']
]])
headers {
contentType(applicationJson())
}
bodyMatchers {
jsonPath('''$[0][0].access_token''', byEquality())
}
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
and:
builder.appendTo(blockBuilder)
String test = blockBuilder.toString()
when:
SyntaxChecker.tryToCompile(methodBuilderName, test)
then:
test.contains('assertThat(parsedJson.read("' + rootElement + '[0][0].access_token", String.class)).isEqualTo("123")')
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) } | '$'
}
@Issue('#229')
def 'should work for matchers and body with json array[#methodBuilderName]'() {
given:
Contract contractDsl = Contract.make {
request {
method 'GET'
url '/api/v1/xxxx'
body(12000)
}
response {
status OK()
body([[
[access_token: '123']
]])
headers {
contentType(applicationJson())
}
bodyMatchers {
jsonPath('''$[0][0].access_token''', byEquality())
}
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
and:
builder.appendTo(blockBuilder)
String test = blockBuilder.toString()
when:
SyntaxChecker.tryToCompile(methodBuilderName, test)
then:
test.contains('assertThat(parsedJson.read("' + rootElement + '[0][0].access_token", String.class)).isEqualTo("123")')
where:
methodBuilderName | methodBuilder | rootElement
HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) } | '$'
JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) } | '\\$'
JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties) } | '$'
}
@Issue("#391")
def "should work for matchers and body with multiline string for [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
name "ISSUE 391"
method 'GET'
urlPath '/item/factsheet?size=2&page=1'
headers { header "accept", "application/...json" }
}
response {
status OK()
body("""
@Issue('#391')
def 'should work for matchers and body with multiline string for [#methodBuilderName]'() {
given:
Contract contractDsl = Contract.make {
request {
name 'ISSUE 391'
method 'GET'
urlPath '/item/factsheet?size=2&page=1'
headers { header 'accept', 'application/...json' }
}
response {
status OK()
body('''
{
"items": [
{
@@ -460,73 +468,75 @@ class MockMvcMethodBodyBuilderWithMatchersSpec extends Specification implements
}
]
}
""")
bodyMatchers {
jsonPath('$.items[*].id', byRegex(nonBlank()))
jsonPath('$.items[*].title', byRegex(nonBlank()))
jsonPath('$.items[*]', byType { minOccurrence(2); maxOccurrence(2) })
}
headers {header "content-type", "application/...json;charset=UTF-8"}
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
and:
builder.appendTo(blockBuilder)
String test = blockBuilder.toString()
when:
SyntaxChecker.tryToCompile(methodBuilderName, test)
then:
!test.contains('''assertThatJson(parsedJson).array("['items']").isEmpty()''')
where:
methodBuilderName | methodBuilder
"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) }
}
''')
bodyMatchers {
jsonPath('$.items[*].id', byRegex(nonBlank()))
jsonPath('$.items[*].title', byRegex(nonBlank()))
jsonPath('$.items[*]', byType { minOccurrence(2); maxOccurrence(2) })
}
headers { header 'content-type', 'application/...json;charset=UTF-8' }
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
and:
builder.appendTo(blockBuilder)
String test = blockBuilder.toString()
when:
SyntaxChecker.tryToCompile(methodBuilderName, test)
then:
!test.contains('''assertThatJson(parsedJson).array("['items']").isEmpty()''')
where:
methodBuilderName | methodBuilder
HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties) }
MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) }
JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) }
JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) }
WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties) }
}
@Issue("#391")
def "should work for matchers and body with multiline string with map body for [#methodBuilderName]"() {
given:
Contract contractDsl = Contract.make {
request {
name "ISSUE 391"
method 'GET'
urlPath '/item/factsheet?size=2&page=1'
headers { header "accept", "application/...json" }
}
response {
status OK()
body([
"items": [
"id" : "35309",
"title": "lorem ipsum"
]
])
bodyMatchers {
jsonPath('$.items[*].id', byRegex(nonBlank()))
jsonPath('$.items[*].title', byRegex(nonBlank()))
jsonPath('$.items[*]', byType { minOccurrence(2); maxOccurrence(2) })
}
headers {header "content-type", "application/...json;charset=UTF-8"}
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
and:
builder.appendTo(blockBuilder)
String test = blockBuilder.toString()
when:
SyntaxChecker.tryToCompile(methodBuilderName, test)
then:
!test.contains('''assertThatJson(parsedJson).array("['items']").isEmpty()''')
where:
methodBuilderName | methodBuilder
"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) }
}
@Issue('#391')
def 'should work for matchers and body with multiline string with map body for [#methodBuilderName]'() {
given:
Contract contractDsl = Contract.make {
request {
name "ISSUE 391"
method 'GET'
urlPath '/item/factsheet?size=2&page=1'
headers { header 'accept', 'application/...json' }
}
response {
status OK()
body([
"items": [
"id" : "35309",
"title": "lorem ipsum"
]
])
bodyMatchers {
jsonPath('$.items[*].id', byRegex(nonBlank()))
jsonPath('$.items[*].title', byRegex(nonBlank()))
jsonPath('$.items[*]', byType { minOccurrence(2); maxOccurrence(2) })
}
headers { header 'content-type', 'application/...json;charset=UTF-8' }
}
}
MethodBodyBuilder builder = methodBuilder(contractDsl)
BlockBuilder blockBuilder = new BlockBuilder(" ")
and:
builder.appendTo(blockBuilder)
String test = blockBuilder.toString()
when:
SyntaxChecker.tryToCompile(methodBuilderName, test)
then:
!test.contains('''assertThatJson(parsedJson).array("['items']").isEmpty()''')
where:
methodBuilderName | methodBuilder
HttpSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new HttpSpockMethodRequestProcessingBodyBuilder(dsl, properties) }
MockMvcJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new MockMvcJUnitMethodBodyBuilder(dsl, properties) }
JaxRsClientSpockMethodRequestProcessingBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientSpockMethodRequestProcessingBodyBuilder(dsl, properties) }
JaxRsClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new JaxRsClientJUnitMethodBodyBuilder(dsl, properties) }
WebTestClientJUnitMethodBodyBuilder.simpleName | { Contract dsl -> new WebTestClientJUnitMethodBodyBuilder(dsl, properties) }
}
}

View File

@@ -1,17 +1,14 @@
package org.springframework.cloud.contract.verifier.util
import java.lang.reflect.Method
import javax.inject.Inject
import javax.ws.rs.client.Entity
import javax.ws.rs.client.WebTarget
import javax.ws.rs.core.Response
import com.jayway.jsonpath.DocumentContext
import com.jayway.jsonpath.JsonPath
import com.toomuchcoding.jsonassert.JsonAssertion
import groovy.transform.CompileStatic
import io.restassured.RestAssured
import io.restassured.module.mockmvc.RestAssuredMockMvc
import io.restassured.module.webtestclient.RestAssuredWebTestClient
import io.restassured.module.webtestclient.response.WebTestClientResponse
import io.restassured.module.webtestclient.specification.WebTestClientRequestSpecification
import io.restassured.response.ResponseOptions
import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer
@@ -19,7 +16,6 @@ import org.codehaus.groovy.control.customizers.ImportCustomizer
import org.junit.Rule
import org.junit.Test
import org.mdkt.compiler.InMemoryJavaCompiler
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.assertion.SpringCloudContractAssertions
import org.springframework.cloud.contract.verifier.messaging.internal.ContractVerifierMessage
@@ -28,6 +24,12 @@ import org.springframework.cloud.contract.verifier.messaging.internal.ContractVe
import org.springframework.cloud.contract.verifier.messaging.util.ContractVerifierMessagingUtil
import org.springframework.util.ReflectionUtils
import javax.inject.Inject
import javax.ws.rs.client.Entity
import javax.ws.rs.client.WebTarget
import javax.ws.rs.core.Response
import java.lang.reflect.Method
/**
* checking the syntax of produced scripts
*/
@@ -40,8 +42,8 @@ class SyntaxChecker {
private static final String[] DEFAULT_IMPORTS = [
Contract.name,
ResponseOptions.name,
"io.restassured.module.mockmvc.specification.*",
"io.restassured.module.mockmvc.*",
'io.restassured.module.mockmvc.specification.*',
'io.restassured.module.mockmvc.*',
Test.name,
Rule.name,
DocumentContext.name,
@@ -51,7 +53,9 @@ class SyntaxChecker {
ContractVerifierMessage.name,
ContractVerifierMessaging.name,
WebTarget.name,
Response.name
Response.name,
WebTestClientRequestSpecification.name,
WebTestClientResponse.name
]
private static final String DEFAULT_IMPORTS_AS_STRING = DEFAULT_IMPORTS.collect {
@@ -68,20 +72,28 @@ class SyntaxChecker {
"${SpringCloudContractAssertions.name}.assertThat"
].collect { "import static ${it};"}.join("\n")
private static final String WEB_TEST_CLIENT_STATIC_IMPORTS = [
"${RestAssuredWebTestClient.name}.*",
"${Entity.name}.*",
"${ContractVerifierMessagingUtil.name}.headers",
"${JsonAssertion.name}.assertThatJson",
"${SpringCloudContractAssertions.name}.assertThat"
].collect { "import static ${it};" }.join("\n")
static void tryToCompile(String builderName, String test) {
if (builderName.toLowerCase().contains("spock")) {
tryToCompileGroovy(test)
tryToCompileGroovy(builderName, test)
} else {
tryToCompileJava(test)
tryToCompileJava(builderName, test)
}
}
static void tryToRun(String builderName, String test) {
if (builderName.toLowerCase().contains("spock")) {
Script script = tryToCompileGroovy(test)
Script script = tryToCompileGroovy(builderName, test)
script.run()
} else {
Class clazz = tryToCompileJava(test)
Class clazz = tryToCompileJava(builderName, test)
Method method = ReflectionUtils.findMethod(clazz, "method")
method.invoke(clazz.newInstance())
}
@@ -90,13 +102,13 @@ class SyntaxChecker {
// no static compilation due to bug in Groovy https://issues.apache.org/jira/browse/GROOVY-8055
static void tryToCompileWithoutCompileStatic(String builderName, String test) {
if (builderName.toLowerCase().contains("spock")) {
tryToCompileGroovy(test, false)
tryToCompileGroovy(builderName, test, false)
} else {
tryToCompileJava(test)
tryToCompileJava(builderName, test)
}
}
static Script tryToCompileGroovy(String test, boolean compileStatic = true) {
static Script tryToCompileGroovy(String builderName, String test, boolean compileStatic = true) {
def imports = new ImportCustomizer()
CompilerConfiguration configuration = new CompilerConfiguration()
if (compileStatic) {
@@ -106,7 +118,7 @@ class SyntaxChecker {
configuration.addCompilationCustomizers(imports)
StringBuilder sourceCode = new StringBuilder()
sourceCode.append("${DEFAULT_IMPORTS_AS_STRING}\n")
sourceCode.append("${STATIC_IMPORTS}\n")
sourceCode.append(getStaticImports(builderName))
sourceCode.append("\n")
sourceCode.append("WebTarget webTarget")
sourceCode.append("\n")
@@ -114,7 +126,14 @@ class SyntaxChecker {
return new GroovyShell(SyntaxChecker.classLoader, configuration).parse(sourceCode.toString())
}
static Class tryToCompileJava(String test) {
private static GString getStaticImports(String builderName) {
if (builderName.toLowerCase().contains('webtestclient')) {
return "$WEB_TEST_CLIENT_STATIC_IMPORTS\n"
}
return "$STATIC_IMPORTS\n"
}
static Class tryToCompileJava(String builderName, String test) {
Random random = new Random()
int first = Math.abs(random.nextInt())
int hashCode = Math.abs(test.hashCode())
@@ -123,7 +142,7 @@ class SyntaxChecker {
String fqnClassName = "com.example.${className}"
sourceCode.append("package com.example;\n")
sourceCode.append("${DEFAULT_IMPORTS_AS_STRING}\n")
sourceCode.append("${STATIC_IMPORTS}\n")
sourceCode.append(getStaticImports(builderName))
sourceCode.append("\n")
sourceCode.append("public class ${className} {\n")
sourceCode.append("\n")