Added option of multiple test base classes

without this change the user was forced to use a single base class for all of the generated tests. It could become problematic after some time.

With this change we provide a range of options of providing different base classes for different contracts.

fixes #16
This commit is contained in:
Marcin Grzejszczak
2016-09-23 15:39:12 +02:00
parent 82a3da2b37
commit e8917940ec
33 changed files with 746 additions and 33 deletions

View File

@@ -818,4 +818,9 @@ and contracts present under the `com/example/server` will be picked as the ones
tests and the stubs. Due to this convention the producer team will know which consumer teams will be broken
when some incompatible changes are done.
The rest of the flow looks the same.
The rest of the flow looks the same.
===== Can I have multiple base classes for tests?
Yes! Check out the https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract.html#different_base_classes_for_contracts[Different base classes for contracts] sections
of either Gradle or Maven plugins.

View File

@@ -182,7 +182,9 @@ contracts {
- **imports** - array with imports that should be included in generated tests (for example ['org.myorg.Matchers']). By default empty array []
- **staticImports** - array with static imports that should be included in generated tests(for example ['org.myorg.Matchers.*']). By default empty array []
- **basePackageForTests** - specifies base package for all generated tests. By default set to org.springframework.cloud.verifier.tests
- **baseClassForTests** - base class for generated tests. By default `spock.lang.Specification` if using Spock tests.
- **baseClassForTests** - base class for all generated tests. By default `spock.lang.Specification` if using Spock tests.
- **packageWithBaseClasses** - instead of providing a fixed value for base class you can provide a package where all the base classes lay. Takes precedence over **baseClassForTests**.
- **baseClassMappings** - explicitly map contract package to a FQN of a base class. Takes precedence over **packageWithBaseClasses** and **baseClassForTests**.
- **ruleClassForTests** - specifies Rule which should be added to generated test classes.
- **ignoredFiles** - Ant matcher allowing defining stub files for which processing should be skipped. By default empty array []
- **contractsDslDir** - directory containing contracts written using the GroovyDSL. By default `$rootDir/src/test/resources/contracts`
@@ -196,7 +198,7 @@ The following properties are used when you want to provide where the JAR with co
- **contractsPath** - if contract deps are downloaded will default to `groupid/artifactid` where `groupid` will be slash separated. Otherwise will scan contracts under provided directory
- **contractsWorkOffline** - in order not to download the dependencies each time you can download them once and work offline afterwards (reuse local Maven repo)
====== Base class for tests
====== Single base class for all tests
When using Spring Cloud Contract Verifier in default MockMvc you need to create a base specification for all generated acceptance tests. In this class you need to point to endpoint which should be verified.
@@ -208,6 +210,43 @@ include::{plugins_path}/spring-cloud-contract-gradle-plugin/src/test/resources/f
In case of using `Explicit` mode, you can use base class to initialize the whole tested app similarly as in regular integration tests. In case of `JAXRSCLIENT` mode this base class
should also contain `protected WebTarget webTarget` field, right now the only option to test JAX-RS API is to start a web server.
====== Different base classes for contracts
If your base classes differ between contracts you can tell the Spring Cloud Contract plugin which class should get
extended by the autogenerated tests. You have two options:
- follow a convention by providing the `packageWithBaseClasses`
- provide explicit mapping via `baseClassMappings`
*Convention*
The convention is such that if you have a contract under e.g. `src/test/resources/contract/foo/bar/baz/` and provide the value of the `packageWithBaseClasses` property
to `com.example.base` then we will assume that there is a `BarBazBase` class under `com.example.base` package. In other words we take last two parts of package
if they exist and form a class with a `Base` suffix. Takes precedence over **baseClassForTests**. Example of usage in the `contracts` closure:
[source,groovy,indent=0]
----
include::{plugins_path}/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierSpec.groovy[tags=package_with_base_classes,indent=0]
----
*Mapping*
You can manually map a regular expression of the contract's package to fully qualified name of the base class for the matched contract.
Let's take a look at the following example:
[source,groovy,indent=0]
----
include::{plugins_path}/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierSpec.groovy[tags=base_class_mappings,indent=0]
----
Let's assume that you have contracts under
- `src/test/resources/contract/com/`
- `src/test/resources/contract/foo/`
By providing the `baseClassForTests` we have a fallback in case mapping didn't succeed (you could also provide
the `packageWithBaseClasses` as fallback). That way the tests generated from `src/test/resources/contract/com/` contracts
will be extending the `com.example.ComBase` whereas the rest of tests will extend `com.example.FooBase`.
===== Invoking generated tests
To ensure that provider side is complaint with defined contracts, you need to invoke:
@@ -341,6 +380,15 @@ To change default configuration just add `configuration` section to plugin defin
- **baseClassForTests** - base class for generated tests. By default `spock.lang.Specification` if using Spock tests.
- **contractsDir** - directory containing contracts written using the GroovyDSL. By default `/src/test/resources/contracts`.
- **testFramework** - the target test framework to be used; currently Spock and JUnit are supported with JUnit being the default framework
- **packageWithBaseClasses** - instead of providing a fixed value for base class you can provide a package where all the base classes lay.
The convention is such that if you have a contract under `src/test/resources/contract/foo/bar/baz/` and provide the value of this property
to `com.example.base` then we will assume that there is a `BarBazBase` class under `com.example.base` package. Takes precedence
over **baseClassForTests**
- **baseClassMappings** - list of base class mappings that where you have to provide `contractPackageRegex` which is checked
against the package in which the contract lays and `baseClassFQN` that maps to fully qualified name of the base class for the matched
contract. If you have a contract under `src/test/resources/contract/foo/bar/baz/` and map the property `.*` -> `com.example.base.BaseClass` then
the test class generated from these contracts will extend `com.example.base.BaseClass`. Takes precedence over **packageWithBaseClasses**
and **baseClassForTests**.
If you want to download your contract definitions from a Maven repository you can use
@@ -351,7 +399,7 @@ If you want to download your contract definitions from a Maven repository you ca
For complete information take a look at https://cloud.spring.io/spring-cloud-contract/spring-cloud-contract-maven-plugin/plugin-info.html[Plugin Documentation]
====== Base class for tests
====== Single base class for all tests
When using Spring Cloud Contract Verifier in default MockMvc you need to create a base specification for all generated acceptance tests.
In this class you need to point to endpoint which should be verified.
@@ -373,6 +421,44 @@ class MvcSpec extends Specification {
In case of using `Explicit` mode, you can use base class to initialize the whole tested app similarly as in regular integration tests. In case of `JAXRSCLIENT` mode this base class should also contain `protected WebTarget webTarget` field, right now the only option to test JAX-RS API is to start a web server.
====== Different base classes for contracts
If your base classes differ between contracts you can tell the Spring Cloud Contract plugin which class should get
extended by the autogenerated tests. You have two options:
- follow a convention by providing the `packageWithBaseClasses`
- provide explicit mapping via `baseClassMappings`
*Convention*
The convention is such that if you have a contract under e.g. `src/test/resources/contract/hello/v1/` and provide the value of the `packageWithBaseClasses` property
to `hello` then we will assume that there is a `HelloV1Base` class under `hello` package. In other words we take last two parts of package
if they exist and form a class with a `Base` suffix. Takes precedence over **baseClassForTests**. Example of usage in the `contracts` closure:
[source,xml,indent=0]
----
include::{plugins_path}/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/pom.xml[tags=convention,indent=0]
----
*Mapping*
You can manually map a regular expression of the contract's package to fully qualified name of the base class for the matched contract.
You have to provide a list `baseClassMappings` of `baseClassMapping` that takes a `contractPackageRegex` to `baseClassFQN` mapping.
Let's take a look at the following example:
[source,xml,indent=0]
----
include::{plugins_path}/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/pom.xml[tags=mapping,indent=0]
----
Let's assume that you have contracts under
- `src/test/resources/contract/com/`
- `src/test/resources/contract/foo/`
By providing the `baseClassForTests` we have a fallback in case mapping didn't succeed (you could also provide
the `packageWithBaseClasses` as fallback). That way the tests generated from `src/test/resources/contract/com/` contracts
will be extending the `com.example.ComBase` whereas the rest of tests will extend `com.example.FooBase`.
===== Invoking generated tests
Spring Cloud Contract Verifier Maven Plugin generates verification code into directory `/generated-test-sources/contractVerifier` and attach this directory to `testCompile` goal.

View File

@@ -39,7 +39,7 @@ dependencyManagement {
}
contracts {
baseClassForTests = 'com.example.fraud.MvcTest'
packageWithBaseClasses = 'com.example.fraud'
}
dependencies {

View File

@@ -76,7 +76,7 @@
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<baseClassForTests>com.example.fraud.MvcTest</baseClassForTests>
<packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
</configuration>
</plugin>
<!-- end::contract_maven_plugin[] -->

View File

@@ -5,7 +5,7 @@ import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
import org.junit.Before;
public class MvcTest {
public class FraudBase {
@Before
public void setup() {

View File

@@ -39,7 +39,9 @@ ext {
}
contracts {
baseClassForTests = 'com.example.source.SensorSourceTestBase'
baseClassMappings {
baseClassMapping('.*', 'com.example.source.SensorSourceTestBase')
}
stubsOutputDir = stubsOutputDirRoot
}

View File

@@ -70,7 +70,12 @@
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<baseClassForTests>com.example.source.SensorSourceTestBase</baseClassForTests>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*</contractPackageRegex>
<baseClassFQN>com.example.source.SensorSourceTestBase</baseClassFQN>
</baseClassMapping>
</baseClassMappings>
</configuration>
</plugin>

View File

@@ -112,11 +112,38 @@ class ContractVerifierExtension {
*/
boolean contractsWorkOffline
/**
* A package that contains all the base clases for generated tests. If your contract resides in a location
* {@code src/test/resources/contracts/com/example/v1/} and you provide the {@code packageWithBaseClasses}
* value to {@code com.example.contracts.base} then we will search for a test source file that will
* have the package {@code com.example.contracts.base} and name {@code ExampleV1Base}. As you can see
* it will take the two last folders to and attach {@code Base} to its name.
*/
String packageWithBaseClasses
/**
* A way to override any base class mappings. The keys are regular expressions on the package name
* and the values FQN to a base class for that given expression.
* </p>
* Example of a mapping
* </p>
* {@code .*.com.example.v1..*} -> {@code com.example.SomeBaseClass}
* </p>
* When a contract's package matches the provided regular expression then extending class will be the one
* provided in the map - in this case {@code com.example.SomeBaseClass}
*/
Map<String, String> baseClassMappings = [:]
void contractDependency(@DelegatesTo(Dependency) Closure closure) {
closure.delegate = contractDependency
closure.call()
}
void baseClassMappings(@DelegatesTo(BaseClassMapping) Closure closure) {
closure.delegate = new BaseClassMapping(baseClassMappings)
closure.call()
}
static class Dependency {
String groupId
String artifactId
@@ -124,4 +151,20 @@ class ContractVerifierExtension {
String version
String stringNotation
}
static class BaseClassMapping {
private final Map<String, String> delegate
BaseClassMapping(Map<String, String> delegate) {
this.delegate = delegate
}
void baseClassMapping(String packageRegex, String fqnBaseClass) {
this.delegate[packageRegex] = fqnBaseClass
}
void baseClassMapping(Map mapping) {
this.delegate.putAll(mapping)
}
}
}

View File

@@ -25,7 +25,9 @@ class ExtensionToProperties {
generatedTestSourcesDir: extension.generatedTestSourcesDir,
stubsOutputDir: extension.stubsOutputDir,
stubsSuffix: extension.stubsSuffix,
assertJsonSize: extension.assertJsonSize
assertJsonSize: extension.assertJsonSize,
packageWithBaseClasses: extension.packageWithBaseClasses,
baseClassMappings: extension.baseClassMappings
)
}
}

View File

@@ -33,8 +33,8 @@ abstract class ContractVerifierIntegrationSpec extends Specification {
public static final String SPOCK = "targetFramework = 'Spock'"
public static final String JUNIT = "targetFramework = 'JUnit'"
public static final String MVC_SPEC = "baseClassForTests = 'org.springframework.cloud.MvcSpec'"
public static final String MVC_TEST = "baseClassForTests = 'org.springframework.cloud.MvcTest'"
public static final String MVC_SPEC = "'org.springframework.cloud.MvcSpec'"
public static final String MVC_TEST = "'org.springframework.cloud.MvcTest'"
protected static final boolean WORK_OFFLINE = Boolean.parseBoolean(System.getProperty('WORK_OFFLINE', 'false'))
File testProjectDir

View File

@@ -138,4 +138,25 @@ class ContractVerifierSpec extends Specification {
it.group == "org.assertj" && it.name == "assertj-core"
} != null
}
def "should compile"() {
given:
ContractVerifierExtension extension = new ContractVerifierExtension()
extension.with {
// tag::package_with_base_classes[]
packageWithBaseClasses = 'com.example.base'
// end::package_with_base_classes[]
// tag::base_class_mappings[]
baseClassForTests = "com.example.FooBase"
baseClassMappings {
baseClassMapping('.*/com/.*', 'com.example.ComBase')
baseClassMapping('.*/bar/.*':'com.example.BarBase')
}
// end::base_class_mappings[]
}
expect:
extension
}
}

View File

@@ -81,7 +81,12 @@ configure([project(':fraudDetectionService'), project(':loanApplicationService')
targetFramework = 'Spock'
// end::target_framework[]
testMode = 'MockMvc'
baseClassForTests = 'org.springframework.cloud.MvcSpec'
//baseClassForTests = 'org.springframework.cloud.MvcSpec'
// tag::base_class_mapping[]
baseClassMappings {
baseClassMapping('.*', 'org.springframework.cloud.MvcSpec')
}
// end::base_class_mapping[]
contractsDslDir = file("${project.projectDir.absolutePath}/mappings/")
generatedTestSourcesDir = file("${project.buildDir}/generated-test-sources/")
stubsOutputDir = stubsOutputDirRoot

View File

@@ -0,0 +1,53 @@
package org.springframework.cloud.contract.maven.verifier;
/**
* Represents a single mapping of regex on package where contracts reside
* to the FQN of the base test class
*
* @author Marcin Grzejszczak
* @since 1.0.0
*/
public class BaseClassMapping {
private String contractPackageRegex;
private String baseClassFQN;
public String getContractPackageRegex() {
return this.contractPackageRegex;
}
public void setContractPackageRegex(String contractPackageRegex) {
this.contractPackageRegex = contractPackageRegex;
}
public String getBaseClassFQN() {
return this.baseClassFQN;
}
public void setBaseClassFQN(String baseClassFQN) {
this.baseClassFQN = baseClassFQN;
}
@Override public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
BaseClassMapping that = (BaseClassMapping) o;
if (this.contractPackageRegex != null ?
!this.contractPackageRegex.equals(that.contractPackageRegex) :
that.contractPackageRegex != null)
return false;
return this.baseClassFQN != null ?
this.baseClassFQN.equals(that.baseClassFQN) :
that.baseClassFQN == null;
}
@Override public int hashCode() {
int result = this.contractPackageRegex != null ? this.contractPackageRegex.hashCode() : 0;
result = 31 * result + (this.baseClassFQN != null ? this.baseClassFQN.hashCode() : 0);
return result;
}
}

View File

@@ -134,7 +134,6 @@ public class ConvertMojo extends AbstractMojo {
new CopyContracts(this.project, this.mavenSession, this.mavenResourcesFiltering)
.copy(contractsDirectory, this.outputDirectory);
config.setContractsDslDir(isInsideProject() ? contractsDirectory : this.source);
config.setStubsOutputDir(
isInsideProject() ? new File(this.outputDirectory, "mappings") : this.destination);

View File

@@ -16,8 +16,9 @@
package org.springframework.cloud.contract.maven.verifier;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import org.apache.maven.model.Dependency;
@@ -140,6 +141,30 @@ public class GenerateTestsMojo extends AbstractMojo {
@Parameter(property = "contractsWorkOffline", defaultValue = "false")
private boolean contractsWorkOffline;
/**
* A package that contains all the base clases for generated tests. If your contract resides in a location
* {@code src/test/resources/contracts/com/example/v1/} and you provide the {@code packageWithBaseClasses}
* value to {@code com.example.contracts.base} then we will search for a test source file that will
* have the package {@code com.example.contracts.base} and name {@code ExampleV1Base}. As you can see
* it will take the two last folders to and attach {@code Base} to its name.
*/
@Parameter(property = "packageWithBaseClasses")
private String packageWithBaseClasses;
/**
* A way to override any base class mappings. The keys are regular expressions on the package name of the contract
* and the values FQN to a base class for that given expression.
* </p>
* Example of a mapping
* </p>
* {@code .*.com.example.v1..*} -> {@code com.example.SomeBaseClass}
* </p>
* When a contract's package matches the provided regular expression then extending class will be the one
* provided in the map - in this case {@code com.example.SomeBaseClass}
*/
@Parameter(property = "baseClassMappings")
private List<BaseClassMapping> baseClassMappings;
private final AetherStubDownloaderFactory aetherStubDownloaderFactory;
@Inject
@@ -175,6 +200,10 @@ public class GenerateTestsMojo extends AbstractMojo {
config.setIgnoredFiles(this.ignoredFiles);
config.setExcludedFiles(this.excludedFiles);
config.setAssertJsonSize(this.assertJsonSize);
config.setPackageWithBaseClasses(this.packageWithBaseClasses);
if (this.baseClassMappings != null) {
config.setBaseClassMappings(mappingsToMap());
}
this.project.addTestCompileSourceRoot(this.generatedTestSourcesDir.getAbsolutePath());
if (getLog().isInfoEnabled()) {
getLog().info(
@@ -195,6 +224,17 @@ public class GenerateTestsMojo extends AbstractMojo {
}
}
public Map<String, String> mappingsToMap() {
Map<String, String> map = new HashMap<>();
if (this.baseClassMappings == null) {
return map;
}
for (BaseClassMapping mapping : this.baseClassMappings) {
map.put(mapping.getContractPackageRegex(), mapping.getBaseClassFQN());
}
return map;
}
public List<String> getExcludedFiles() {
return this.excludedFiles;
}

View File

@@ -162,4 +162,51 @@ public class PluginUnitTest {
assertFilesPresent(basedir, "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/com/example/server/client1/ContractsTest.java");
}
@Test
public void shouldGenerateContractTestsWithBaseClassResolvedFromConvention() throws Exception {
File basedir = this.resources.getBasedir("basic-generated-baseclass");
this.maven.executeMojo(basedir, "generateTests", newParameter("testFramework", "JUNIT"));
String path = "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/hello/V1Test.java";
assertFilesPresent(basedir, path);
File test = new File(basedir, path);
then(FileUtils.readFileToString(test)).contains("extends HelloV1Base").contains("import hello.HelloV1Base");
}
@Test
public void shouldGenerateContractTestsWithBaseClassResolvedFromConventionForSpock() throws Exception {
File basedir = this.resources.getBasedir("basic-generated-baseclass");
this.maven.executeMojo(basedir, "generateTests", newParameter("testFramework", "SPOCK"));
String path = "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/hello/V1Spec.groovy";
assertFilesPresent(basedir, path);
File test = new File(basedir, path);
then(FileUtils.readFileToString(test)).contains("extends HelloV1Base").contains("import hello.HelloV1Base");
}
@Test
public void shouldGenerateContractTestsWithBaseClassResolvedFromMapping() throws Exception {
File basedir = this.resources.getBasedir("basic-baseclass-from-mappings");
this.maven.executeMojo(basedir, "generateTests", newParameter("testFramework", "JUNIT"));
String path = "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/com/hello/V1Test.java";
assertFilesPresent(basedir, path);
File test = new File(basedir, path);
then(FileUtils.readFileToString(test)).contains("extends TestBase").contains("import com.example.TestBase");
}
@Test
public void shouldGenerateContractTestsWithBaseClassResolvedFromMappingNameForSpock() throws Exception {
File basedir = this.resources.getBasedir("basic-baseclass-from-mappings");
this.maven.executeMojo(basedir, "generateTests", newParameter("testFramework", "SPOCK"));
String path = "target/generated-test-sources/contracts/org/springframework/cloud/contract/verifier/tests/com/hello/V1Spec.groovy";
assertFilesPresent(basedir, path);
File test = new File(basedir, path);
then(FileUtils.readFileToString(test)).contains("extends TestBase").contains("import com.example.TestBase");
}
}

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2013-2016 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>org.springframework.cloud.verifier.sample</groupId>
<artifactId>sample-project</artifactId>
<version>0.1</version>
<build>
<plugins>
<!-- tag::mapping[] -->
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<baseClassForTests>com.example.FooBase</baseClassForTests>
<baseClassMappings>
<baseClassMapping>
<contractPackageRegex>.*com.*</contractPackageRegex>
<baseClassFQN>com.example.TestBase</baseClassFQN>
</baseClassMapping >
</baseClassMappings>
</configuration>
</plugin>
<!-- end::mapping[] -->
</plugins>
</build>
</project>

View File

@@ -0,0 +1,37 @@
/**
*
* Copyright 2013-2016 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.
*/
org.springframework.cloud.contract.spec.Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}

View File

@@ -0,0 +1,34 @@
/**
*
* Copyright 2013-2016 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.
*/
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url('/users') {
}
headers {
header 'Content-Type': 'application/json'
}
body '''{ "login" : "john", "name": "John The Contract" }'''
}
response {
status 200
headers {
header 'Location': '/users/john'
}
}
}

View File

@@ -0,0 +1,34 @@
/**
*
* Copyright 2013-2016 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.
*/
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url('/users') {
}
headers {
header 'Content-Type': 'application/json'
}
}
response {
status 200
headers {
header 'Location': '/users/john'
}
body '''{ "list" : [ "login", "john", "name", "John The Contract" ] }'''
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2013-2016 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>org.springframework.cloud.verifier.sample</groupId>
<artifactId>sample-project</artifactId>
<version>0.1</version>
<build>
<plugins>
<!-- tag::convention[] -->
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<configuration>
<packageWithBaseClasses>hello</packageWithBaseClasses>
</configuration>
</plugin>
<!-- tag::convention[] -->
</plugins>
</build>
</project>

View File

@@ -0,0 +1,37 @@
/**
*
* Copyright 2013-2016 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.
*/
org.springframework.cloud.contract.spec.Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}

View File

@@ -0,0 +1,34 @@
/**
*
* Copyright 2013-2016 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.
*/
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url('/users') {
}
headers {
header 'Content-Type': 'application/json'
}
body '''{ "login" : "john", "name": "John The Contract" }'''
}
response {
status 200
headers {
header 'Location': '/users/john'
}
}
}

View File

@@ -0,0 +1,34 @@
/**
*
* Copyright 2013-2016 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.
*/
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url('/users') {
}
headers {
header 'Content-Type': 'application/json'
}
}
response {
status 200
headers {
header 'Location': '/users/john'
}
body '''{ "list" : [ "login", "john", "name", "John The Contract" ] }'''
}
}

View File

@@ -87,7 +87,7 @@ class TestGenerator {
if (contracts.size()) {
def className = afterLast(includedDirectoryRelativePath.toString(), File.separator) + resolveNameSuffix()
def packageName = buildPackage(basePackageNameForClass, includedDirectoryRelativePath)
def classBytes = generator.buildClass(contracts, className, packageName).getBytes(StandardCharsets.UTF_8)
def classBytes = generator.buildClass(contracts, className, packageName, includedDirectoryRelativePath).getBytes(StandardCharsets.UTF_8)
saver.saveClassFile(className, basePackageNameForClass, convertIllegalPackageChars(includedDirectoryRelativePath.toString()), classBytes)
counter.incrementAndGet()
}

View File

@@ -57,16 +57,46 @@ class ClassBuilder {
/**
* Returns a {@link ClassBuilder} for the given parameters
*/
static ClassBuilder createClass(String className, String classPackage, ContractVerifierConfigProperties properties) {
static ClassBuilder createClass(String className, String classPackage, ContractVerifierConfigProperties properties,
String includedDirectoryRelativePath) {
String baseClassForTests
if (properties.targetFramework == TestFramework.SPOCK && !properties.baseClassForTests) {
if (properties.targetFramework == TestFramework.SPOCK && !properties.baseClassForTests
&& !properties.packageWithBaseClasses && !properties.baseClassMappings) {
baseClassForTests = 'spock.lang.Specification'
} else {
baseClassForTests = properties.baseClassForTests
baseClassForTests = retrieveBaseClass(properties, includedDirectoryRelativePath)
}
return new ClassBuilder(className, classPackage, baseClassForTests, properties.targetFramework)
}
protected static String retrieveBaseClass(ContractVerifierConfigProperties properties, String includedDirectoryRelativePath) {
String contractPackage = includedDirectoryRelativePath.replace(File.separator, '.')
// package mapping takes super precedence
if (properties.baseClassMappings) {
Map.Entry<String, String> mapping = properties.baseClassMappings.find { String pattern, String fqn ->
return contractPackage.matches(pattern)
}
if (mapping) {
return mapping.value
}
}
if (!properties.packageWithBaseClasses) {
return properties.baseClassForTests
}
String generatedClassName = generateDefaultBaseClassName(contractPackage, properties)
return "${generatedClassName}Base"
}
private static String generateDefaultBaseClassName(String classPackage, ContractVerifierConfigProperties properties) {
String[] splitPackage = classPackage.split("\\.")
if (splitPackage.size() > 1) {
String last = NamesUtil.capitalize(splitPackage[-1])
String butLast = NamesUtil.capitalize(splitPackage[-2])
return "${properties.packageWithBaseClasses}.${butLast}${last}"
}
return "${properties.packageWithBaseClasses}.${NamesUtil.capitalize(splitPackage[0])}"
}
ClassBuilder addImport(String importToAdd) {
imports << importToAdd
return this

View File

@@ -23,14 +23,12 @@ import groovy.util.logging.Slf4j
import org.springframework.cloud.contract.spec.Contract
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import org.springframework.cloud.contract.verifier.config.TestFramework
import org.springframework.cloud.contract.verifier.config.TestMode
import org.springframework.cloud.contract.verifier.file.ContractMetadata
import org.springframework.cloud.contract.verifier.util.ContractVerifierDslConverter
import org.springframework.cloud.contract.verifier.config.TestMode
import static ClassBuilder.createClass
import static MethodBuilder.createTestMethod
import static org.springframework.cloud.contract.verifier.util.NamesUtil.capitalize
/**
* Builds a single test for the given {@link ContractVerifierConfigProperties properties}
*
@@ -53,8 +51,8 @@ class SingleTestGenerator {
* each {@link ContractMetadata}
*/
@PackageScope
String buildClass(Collection<ContractMetadata> listOfFiles, String className, String classPackage) {
ClassBuilder clazz = createClass(capitalize(className), classPackage, configProperties)
String buildClass(Collection<ContractMetadata> listOfFiles, String className, String classPackage, String includedDirectoryRelativePath) {
ClassBuilder clazz = createClass(capitalize(className), classPackage, configProperties, includedDirectoryRelativePath)
if (configProperties.imports) {
configProperties.imports.each {
@@ -112,7 +110,7 @@ class SingleTestGenerator {
}
conditionalImportsAdded = true
}
clazz.addMethod(createTestMethod(key.contract, key.stubsFile, key.groovyDsl, configProperties))
clazz.addMethod(MethodBuilder.createTestMethod(key.contract, key.stubsFile, key.groovyDsl, configProperties))
}
return clazz.build()
}
@@ -123,7 +121,7 @@ class SingleTestGenerator {
if (log.isDebugEnabled()) {
log.debug("Stub content from file [${stubsFile.text}]")
}
org.springframework.cloud.contract.spec.Contract stubContent = ContractVerifierDslConverter.convert(stubsFile)
Contract stubContent = ContractVerifierDslConverter.convert(stubsFile)
TestType testType = (stubContent.input || stubContent.outputMessage) ? TestType.MESSAGING : TestType.HTTP
return [(new ParsedDsl(it, stubContent, stubsFile)): testType]
}

View File

@@ -109,4 +109,26 @@ class ContractVerifierConfigProperties {
*/
String includedContracts = ".*"
/**
* A package that contains all the base clases for generated tests. If your contract resides in a location
* {@code src/test/resources/contracts/com/example/v1/} and you provide the {@code packageWithBaseClasses}
* value to {@code com.example.contracts.base} then we will search for a test source file that will
* have the package {@code com.example.contracts.base} and name {@code ExampleV1Base}. As you can see
* it will take the two last folders to and attach {@code Base} to its name.
*/
String packageWithBaseClasses
/**
* A way to override any base class mappings. The keys are regular expressions on the package name of the contract
* and the values FQN to a base class for that given expression.
* </p>
* Example of a mapping
* </p>
* {@code .*.com.example.v1..*} -> {@code com.example.SomeBaseClass}
* </p>
* When a contract's package matches the provided regular expression then extending class will be the one
* provided in the map - in this case {@code com.example.SomeBaseClass}
*/
Map<String, String> baseClassMappings
}

View File

@@ -34,7 +34,7 @@ class GeneratorScannerSpec extends Specification {
when:
testGenerator.generateTestClasses("org.springframework.cloud.contract.verifier")
then:
6 * classGenerator.buildClass(_, _, _) >> "qwerty"
6 * classGenerator.buildClass(_, _, _, _) >> "qwerty"
}
def "should create class with full package"() {
@@ -45,9 +45,9 @@ class GeneratorScannerSpec extends Specification {
when:
testGenerator.generateTestClasses("org.springframework.cloud.contract.verifier")
then:
1 * classGenerator.buildClass(_, 'exceptionsSpec', 'org.springframework.cloud.contract.verifier') >> "spec"
1 * classGenerator.buildClass(_, 'exceptionsSpec', 'org.springframework.cloud.contract.verifier.v1') >> "spec1"
1 * classGenerator.buildClass(_, 'exceptionsSpec', 'org.springframework.cloud.contract.verifier.v2') >> "spec2"
1 * classGenerator.buildClass(_, 'exceptionsSpec', 'org.springframework.cloud.contract.verifier', _) >> "spec"
1 * classGenerator.buildClass(_, 'exceptionsSpec', 'org.springframework.cloud.contract.verifier.v1', _) >> "spec1"
1 * classGenerator.buildClass(_, 'exceptionsSpec', 'org.springframework.cloud.contract.verifier.v2', _) >> "spec2"
}
}

View File

@@ -0,0 +1,55 @@
package org.springframework.cloud.contract.verifier.builder
import org.springframework.cloud.contract.verifier.config.ContractVerifierConfigProperties
import spock.lang.Specification
/**
* @author Marcin Grzejszczak
*/
class ClassBuilderSpec extends Specification {
def "should return explicit base class if provided and no default package for base classes is provided"() {
given:
ContractVerifierConfigProperties props = new ContractVerifierConfigProperties(baseClassForTests: 'a.b.Class')
expect:
'a.b.Class' == ClassBuilder.retrieveBaseClass(props, 'com/example/foo')
}
def "should return a class from the generated path by taking two last folders when package with base classes is provided"() {
given:
ContractVerifierConfigProperties props = new ContractVerifierConfigProperties(packageWithBaseClasses: 'com.example.base')
String contractRelativeFolder = 'com/example/some/superpackage'
expect:
ClassBuilder.retrieveBaseClass(props, contractRelativeFolder) == 'com.example.base.SomeSuperpackageBase'
}
def "should return a class from the generated path by taking a single folder when package with base classes is provided and there are not enough package elements"() {
given:
ContractVerifierConfigProperties props = new ContractVerifierConfigProperties(packageWithBaseClasses: 'com.example.base')
String contractRelativeFolder = 'superpackage'
expect:
ClassBuilder.retrieveBaseClass(props, contractRelativeFolder) == 'com.example.base.SuperpackageBase'
}
def "should return a class from mappings regardless of other entries if mapping exists"() {
given:
ContractVerifierConfigProperties props = new ContractVerifierConfigProperties(
packageWithBaseClasses: 'com.example.base',
baseClassMappings: ['.*' : 'com.example.base.SuperClass'])
String contractRelativeFolder = 'superpackage'
expect:
ClassBuilder.retrieveBaseClass(props, contractRelativeFolder) == 'com.example.base.SuperClass'
}
def "should return the first matching base class when provided mapping doesn't match"() {
given:
ContractVerifierConfigProperties props = new ContractVerifierConfigProperties(
baseClassForTests: 'a.b.Class',
packageWithBaseClasses: 'com.example.base',
baseClassMappings: ['patternNotMatchingAnything' : 'com.example.base.SuperClass'])
String contractRelativeFolder = 'superpackage'
expect:
ClassBuilder.retrieveBaseClass(props, contractRelativeFolder) == 'com.example.base.SuperpackageBase'
}
}

View File

@@ -68,7 +68,7 @@ class SingleTestGeneratorSpec extends Specification {
SingleTestGenerator testGenerator = new SingleTestGenerator(properties)
when:
String clazz = testGenerator.buildClass([contract], "test", "test")
String clazz = testGenerator.buildClass([contract], "test", "test", 'com/foo')
then:
classStrings.each { clazz.contains(it) }
@@ -90,7 +90,7 @@ class SingleTestGeneratorSpec extends Specification {
SingleTestGenerator testGenerator = new SingleTestGenerator(properties)
when:
String clazz = testGenerator.buildClass([contract], "test", "test")
String clazz = testGenerator.buildClass([contract], "test", "test", 'com/foo')
then:
classStrings.each { clazz.contains(it) }
@@ -133,7 +133,7 @@ class SingleTestGeneratorSpec extends Specification {
SingleTestGenerator testGenerator = new SingleTestGenerator(properties)
when:
String clazz = testGenerator.buildClass([contract, contract2], "test", "test")
String clazz = testGenerator.buildClass([contract, contract2], "test", "test", 'com/foo')
then:
classStrings.each { clazz.contains(it) }