Fixed conflicts

This commit is contained in:
Marcin Grzejszczak
2018-07-27 09:01:36 +02:00
22 changed files with 469 additions and 92 deletions

View File

@@ -39,7 +39,7 @@ produced by Spring Cloud Contract Verifier.
* Messaging routes, if you're using a messaging service. We integrate with Spring
Integration, Spring Cloud Stream, Spring AMQP, and Apache Camel. You can also set your
own integrations.
* Acceptance tests (in JUnit or Spock) are used to verify if server-side implementation
* Acceptance tests (in JUnit 4, JUnit 5 or Spock) are used to verify if server-side implementation
of the API is compliant with the contract (__server tests__). A full test is generated by
Spring Cloud Contract Verifier.
@@ -401,8 +401,8 @@ mode for HTTP contracts. However, JAX-RX client and explicit HTTP invocations ca
used. (To do so, change the `testMode` property of the plugin to `JAX-RS` or `EXPLICIT`,
respectively.)
Apart from the default JUnit, you can instead use Spock tests, by setting the plugin
`testFramework` property to `Spock`.
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`.
TIP: You can now also generate WireMock scenarios based on the contracts, by including an
order number followed by an underscore at the beginning of the contract file names.

View File

@@ -344,84 +344,10 @@ contracts in the folder per topic.
===== For Maven Project
To make it possible to work on the producer side we could do the following things (all via Maven plugins):
To make it possible to work on the producer side we should specify an inclusion pattern for
filtering common repository jar by messaging topics we are interested in. ```includedFiles``` property of ```Maven Spring Cloud Contract plugin```
allows us to do that. Also ```contractsPath``` need to be specified since the default path would be the common repository ```groupid/artifactid```.
- Add common repo dependency to your classpath:
[source,xml,indent=0]
----
<dependency>
<groupId>com.example</groupId>
<artifactId>common-repo</artifactId>
<version>${common-repo.version}</version>
</dependency>
----
- Download the JAR with the contracts and unpack the JAR to target:
[source,xml,indent=0]
----
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<id>unpack-dependencies</id>
<phase>process-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>com.example</groupId>
<artifactId>common-repo</artifactId>
<type>jar</type>
<overWrite>false</overWrite>
<outputDirectory>${project.build.directory}/contracts</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
----
- Rip out all the folders we're not interested in:
[source,xml,indent=0]
----
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>1.8</version>
<executions>
<execution>
<phase>process-resources</phase>
<goals>
<goal>run</goal>
</goals>
<configuration>
<tasks>
<delete includeemptydirs="true">
<fileset dir="${project.build.directory}/contracts">
<include name="**/*" />
<!--Producer artifactId-->
<exclude name="**/${project.artifactId}/**" />
<!--List of the supported topics-->
<exclude name="**/${first-topic}/**" />
<exclude name="**/${second-topic}/**" />
</fileset>
</delete>
</tasks>
</configuration>
</execution>
</executions>
</plugin>
----
- Run the contract plugin by pointing to the contracts to the folder under target:
[source,xml,indent=0]
----
@@ -429,16 +355,30 @@ To make it possible to work on the producer side we could do the following thing
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<packageWithBaseClasses>com.example</packageWithBaseClasses>
<contractsMode>REMOTE</contractsMode>
<contractsRepositoryUrl>http://link/to/your/nexus/or/artifactory/or/sth</contractsRepositoryUrl>
<contractDependency>
<groupId>com.example</groupId>
<artifactId>common-repo-with-contracts</artifactId>
<version>+</version>
</contractDependency>
<contractsPath>/</contractsPath>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*intoxication.*</contractPackageRegex>
<baseClassFQN>com.example.intoxication.BeerIntoxicationBase</baseClassFQN>
<contractPackageRegex>.*messaging.*</contractPackageRegex>
<baseClassFQN>com.example.services.MessagingBase</baseClassFQN>
</baseClassMapping>
<baseClassMapping>
<contractPackageRegex>.*rest.*</contractPackageRegex>
<baseClassFQN>com.example.services.TestBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
<contractsDirectory>${project.build.directory}/contracts</contractsDirectory>
<includedFiles>
<includedFile>**/${project.artifactId}/**</includedFile>
<includedFile>**/${first-topic}/**</includedFile>
<includedFile>**/${second-topic}/**</includedFile>
</includedFiles>
</configuration>
</plugin>
----

View File

@@ -58,7 +58,7 @@ class RecursiveFilesConverter {
void processFiles() {
ContractFileScanner scanner = new ContractFileScanner(properties.contractsDslDir,
properties.excludedFiles as Set, [] as Set, properties.includedContracts)
properties.excludedFiles as Set, [] as Set, [] as Set, properties.includedContracts)
ListMultimap<Path, ContractMetadata> contracts = scanner.findContracts()
if (log.isDebugEnabled()) {
log.debug("Found the following contracts $contracts")

View File

@@ -98,6 +98,12 @@ public class GenerateTestsMojo extends AbstractMojo {
@Parameter
private List<String> excludedFiles;
/**
* Patterns that should be taken into account for processing
*/
@Parameter(property = "includedFiles")
private List<String> includedFiles;
/**
* Incubating feature. You can check the size of JSON arrays. If not turned on
* explicitly will be disabled.
@@ -279,6 +285,7 @@ public class GenerateTestsMojo extends AbstractMojo {
config.setStaticImports(this.staticImports);
config.setIgnoredFiles(this.ignoredFiles);
config.setExcludedFiles(this.excludedFiles);
config.setIncludedFiles(this.includedFiles);
config.setAssertJsonSize(this.assertJsonSize);
config.setPackageWithBaseClasses(this.packageWithBaseClasses);
if (this.baseClassMappings != null) {

View File

@@ -305,4 +305,19 @@ public class PluginUnitTest {
then(this.capture.toString()).contains("Skipping pushing stubs to scm since your");
}
@Test
public void shouldGenerateContractTestsForIncludedFilesPattern() throws Exception {
File basedir = this.resources.getBasedir("complex-common-repo-with-messaging");
this.maven.executeMojo(basedir, "generateTests", defaultPackageForTests(),
newParameter("contractsRepositoryUrl",
"file://" + PluginUnitTest.class.getClassLoader()
.getResource("m2repo/repository").getFile()
.replace("/", File.separator)));
assertFilesPresent(basedir,
"target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/common_repo_with_inclusion/kafka_topics/coupon_sent/src/main/resources/contracts/rule_engine_daemon/MessagingTest.java");
assertFilesPresent(basedir,
"target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/common_repo_with_inclusion/reward_rules/src/main/resources/contracts/reward_rules/rest/admin/V1Test.java");
}
}

View File

@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2013-2017 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>social-service</artifactId>
<version>0.1.BUILD-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<configuration>
<contractsMode>REMOTE</contractsMode>
<contractDependency>
<groupId>com.example</groupId>
<artifactId>services-contracts</artifactId>
<version>+</version>
</contractDependency>
<contractsPath>/</contractsPath>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*messaging.*</contractPackageRegex>
<baseClassFQN>com.example.services.TestBase</baseClassFQN>
</baseClassMapping>
<baseClassMapping>
<contractPackageRegex>.*rest.*</contractPackageRegex>
<baseClassFQN>com.example.services.TestBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
<includedFiles>
<includedFile>**/coupon-sent/**</includedFile>
<includedFiles>**/reward-rules/**</includedFiles>
</includedFiles>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,30 @@
/**
*
* Copyright 2013-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.example.services;
import io.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.Before;
public class TestBase {
@Before
public void setup() {
RestAssuredMockMvc.standaloneSetup(new FraudDetectionController());
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2017 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>contracts</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
</project>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata modelVersion="1.1.0">
<groupId>com.example</groupId>
<artifactId>services-contracts</artifactId>
<version>1.0.4-SNAPSHOT</version>
<versioning>
<snapshot>
<localCopy>true</localCopy>
</snapshot>
<lastUpdated>20160916125313</lastUpdated>
<snapshotVersions>
<snapshotVersion>
<extension>jar</extension>
<version>1.0.4-SNAPSHOT</version>
<updated>20160916125313</updated>
</snapshotVersion>
<snapshotVersion>
<extension>pom</extension>
<version>1.0.4-SNAPSHOT</version>
<updated>20160916125313</updated>
</snapshotVersion>
</snapshotVersions>
</versioning>
</metadata>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2017 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>services-contracts</artifactId>
<version>1.0.4-SNAPSHOT</version>
<packaging>pom</packaging>
</project>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2013-2017 the original author or authors.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<metadata>
<groupId>com.example</groupId>
<artifactId>services-contracts</artifactId>
<version>1.0.4-SNAPSHOT</version>
<versioning>
<versions>
<version>1.0.4-SNAPSHOT</version>
</versions>
<lastUpdated>20160409062112</lastUpdated>
</versioning>
</metadata>

View File

@@ -77,6 +77,7 @@ class TestGenerator {
contractFileScanner = new ContractFileScanner(configProperties.contractsDslDir,
configProperties.excludedFiles as Set,
configProperties.ignoredFiles as Set,
configProperties.includedFiles as Set,
this.configProperties.includedContracts)
}

View File

@@ -60,6 +60,11 @@ class ContractVerifierConfigProperties {
*/
List<String> excludedFiles = []
/**
* Patterns that should be taken into account for processing
*/
List<String> includedFiles = []
/**
* Patterns for which generated tests should be @Ignored
*/

View File

@@ -52,12 +52,16 @@ class ContractFileScanner {
private final File baseDir
private final Set<PathMatcher> excludeMatchers
private final Set<PathMatcher> ignoreMatchers
private final Set<PathMatcher> includeMatchers
private final String includeMatcher
ContractFileScanner(File baseDir, Set<String> excluded, Set<String> ignored, String includeMatcher = "") {
ContractFileScanner(File baseDir, Set<String> excluded, Set<String> ignored,
Set<String> included = [],
String includeMatcher = "") {
this.baseDir = baseDir
this.excludeMatchers = processPatterns(excluded ?: [] as Set<String>)
this.ignoreMatchers = processPatterns(ignored ?: [] as Set<String>)
this.includeMatchers = processPatterns(included ?: [] as Set<String>)
this.includeMatcher = includeMatcher
}
@@ -102,7 +106,8 @@ class ContractFileScanner {
boolean excluded = matchesPattern(file, excludeMatchers)
if (!excluded) {
boolean contractFile = isContractFile(file)
boolean included = includeMatcher ? file.absolutePath.matches(includeMatcher) : true
boolean included = includeMatcher ? file.absolutePath.matches(includeMatcher) : true
included = includeMatchers ? matchesPattern(file, includeMatchers) : included
if (contractFile && included) {
addContractToTestGeneration(result, files, file, i, ContractVerifierDslConverter.convertAsCollection(baseDir, file))
} else if (!contractFile && included) {

View File

@@ -34,7 +34,7 @@ class ContractFileScannerSpec extends Specification {
File baseDir = new File(this.getClass().getResource("/directory/with/stubs").toURI())
Set<String> excluded = ["package/**"] as Set
Set<String> ignored = ["other/different/**"] as Set
ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored)
ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set)
when:
ListMultimap<Path, ContractMetadata> result = scanner.findContracts()
then:
@@ -52,7 +52,7 @@ class ContractFileScannerSpec extends Specification {
File baseDir = new File(this.getClass().getResource("/strange_[3.3.3]_directory").toURI())
Set<String> excluded = ["foo/**"] as Set
Set<String> ignored = ["bar/**"] as Set
ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored)
ContractFileScanner scanner = new ContractFileScanner(baseDir, excluded, ignored, [] as Set)
when:
ListMultimap<Path, ContractMetadata> result = scanner.findContracts()
then:
@@ -66,7 +66,7 @@ class ContractFileScannerSpec extends Specification {
def "should find contracts group in scenario"() {
given:
File baseDir = new File(this.getClass().getResource("/directory/with/scenario").toURI())
ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set)
ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, [] as Set)
when:
ListMultimap<Path, ContractMetadata> contracts = scanner.findContracts()
then:
@@ -80,7 +80,7 @@ class ContractFileScannerSpec extends Specification {
def "should find contract files with converters"() {
given:
File baseDir = new File(this.getClass().getResource("/directory/with/mixed").toURI())
ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null) {
ContractFileScanner scanner = new ContractFileScanner(baseDir, null, null, null) {
@Override
protected List<ContractConverter> converters() {
return [new ContractConverter() {
@@ -146,4 +146,22 @@ class ContractFileScannerSpec extends Specification {
result.keySet().size() == 1
result.entries().every { it.value.convertedContract }
}
def "should find contracts for include pattern"() {
given:
File baseDir = new File(this.getClass().getResource("/directory/with/common-messaging").toURI())
Set<String> included = ["social-service/**","**/coupon-collected/**/*V1*"] as Set
ContractFileScanner scanner = new ContractFileScanner(baseDir, [] as Set, [] as Set, included)
when:
ListMultimap<Path, ContractMetadata> result = scanner.findContracts()
then:
result.keySet().size() == 3
result.values().find { (it.path.fileName.toString() == 'couponCollectedEventV1.groovy') }.groupSize==2
result.values().find { (it.convertedContract.first().label == 'couponCollectedV1') }
result.values().findAll { (it.path.fileName.toString() == 'couponCollectedEventV2.groovy') }.isEmpty()
result.values().find { (it.path.fileName.toString() == 'shouldUpdateUserInfo.groovy') }.groupSize==1
result.values().find { (it.path.fileName.toString() == 'shouldReturnEmptyFriendsWhenGetFriends.groovy') }.groupSize==1
result.get(baseDir.toPath().resolve("coupon-sent")).size() == 0
result.get(baseDir.toPath().resolve("reward-rules")).size() == 0
}
}

View File

@@ -0,0 +1,27 @@
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""Should send a message in topic coupon_collected""")
label 'couponCollectedV1'
input {
triggeredBy('couponCollectedSm()')
}
outputMessage {
sentTo('coupon_collected')
body([
receiverSnId: value(consumer("receiver-sn-id"), producer(regex('([^\\W]|-)+'))),
sessionId: value(consumer(7928568413097907541), producer(regex('\\d+'))),
createdTs: value(consumer(1504688949158), producer(regex('\\d+'))),
couponToken: value(consumer("440006-6-1504688949139-xyuzzrx5"), producer(regex('([^\\W]|-)+')))
])
headers {
messagingContentType(applicationJsonUtf8())
}
}
}

View File

@@ -0,0 +1,27 @@
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""Should send a message in topic coupon_collected""")
label 'couponCollectedSm'
input {
triggeredBy('couponCollectedSm()')
}
outputMessage {
sentTo('coupon_collected')
body([
receiverSnId: value(consumer("receiver-sn-id"), producer(regex('([^\\W]|-)+'))),
sessionId: value(consumer(7928568413097907541), producer(regex('\\d+'))),
createdTs: value(consumer(1504688949158), producer(regex('\\d+'))),
couponToken: value(consumer("440006-6-1504688949139-xyuzzrx5"), producer(regex('([^\\W]|-)+')))
])
headers {
messagingContentType(applicationJsonUtf8())
}
}
}

View File

@@ -0,0 +1,27 @@
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""Should send a message in topic coupon_sent""")
label 'couponSentSm'
input {
triggeredBy('couponSentSm()')
}
outputMessage {
sentTo('coupon_sent')
body([
senderUserId: value(consumer(123), producer(regex('\\d+'))),
sessionId: value(consumer(7928568413097907541), producer(regex('\\d+'))),
createdTs: value(consumer(1504688949158), producer(regex('\\d+'))),
couponToken: value(consumer("440006-6-1504688949139-xyuzzrx5"), producer(regex('([^\\W]|-)+')))
])
headers {
messagingContentType(applicationJsonUtf8())
}
}
}

View File

@@ -0,0 +1,38 @@
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description('Should return bet ranges array')
request {
method 'GET'
url('/admin/v1/spin/betRanges')
}
response {
status 200
body(
betRanges: [
[
betRangeId : 3,
fromBetPercent: -1
],
[
betRangeId : 4,
fromBetPercent: 0
],
[
betRangeId : 1,
fromBetPercent: 90
],
[
betRangeId : 2,
fromBetPercent: 130
]
]
)
headers {
contentType(applicationJsonUtf8())
}
}
}

View File

@@ -0,0 +1,30 @@
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
Should return empty array if user has no friends
""")
request {
method 'GET'
urlPath('/SocialServer/social/getFriends')
url('/social/getFriends') {
queryParameters {
parameter 'snId': $(consumer(regex('([^\\W]|-)+')), producer('12345'))
parameter 'snType': $(consumer(regex('\\d+')), producer(2))
}
}
}
response {
status 200
body(
"""
{"friends" : []}
""")
headers {
contentType(applicationJsonUtf8())
}
}
}

View File

@@ -0,0 +1,44 @@
import org.springframework.cloud.contract.spec.Contract
Contract.make {
description("""
User's information should be update if has appropriate age
""")
request {
method 'POST'
url '/test/updateUserInfo'
body([
userId: 123,
age: 25,
firstName: "asd",
lastName: "asd"
])
stubMatchers {
jsonPath('$.userId', byRegex("[1-9]{1}([0-9]{7})"))
jsonPath('$.age', byRegex("(1[89]|[2-9][0-9])"))
jsonPath('$.firstName', byRegex("[a-zA-Z]{2,20}"))
jsonPath('$.lastName', byRegex("[a-zA-Z]{2,20}"))
}
headers {
contentType(applicationJson())
}
}
response {
status 200
body([
userId: fromRequest().body("userId"),
age: fromRequest().body("age"),
firstName: fromRequest().body("firstName"),
lastName: fromRequest().body("lastName")
])
testMatchers {
jsonPath('$.userId', byEquality())
jsonPath('$.age', byEquality())
jsonPath('$.firstName', byEquality())
jsonPath('$.lastName', byEquality())
}
headers {
contentType(applicationJson())
}
}
}