From e8917940ec6b094365861b77db2184963c695f4c Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Fri, 23 Sep 2016 15:39:12 +0200 Subject: [PATCH] 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 --- .../main/asciidoc/verifier/introduction.adoc | 7 +- docs/src/main/asciidoc/verifier/rest.adoc | 92 ++++++++++++++++++- .../standalone/dsl/http-server/build.gradle | 2 +- samples/standalone/dsl/http-server/pom.xml | 2 +- .../fraud/{MvcTest.java => FraudBase.java} | 2 +- .../shouldMarkClientAsFraud.groovy | 0 .../shouldMarkClientAsNotFraud.groovy | 0 .../messaging/stream-source/build.gradle | 4 +- .../messaging/stream-source/pom.xml | 7 +- .../plugin/ContractVerifierExtension.groovy | 43 +++++++++ .../plugin/ExtensionToProperties.groovy | 4 +- .../ContractVerifierIntegrationSpec.groovy | 4 +- .../plugin/ContractVerifierSpec.groovy | 21 +++++ .../scenarioProject/build.gradle | 7 +- .../maven/verifier/BaseClassMapping.java | 53 +++++++++++ .../contract/maven/verifier/ConvertMojo.java | 1 - .../maven/verifier/GenerateTestsMojo.java | 42 ++++++++- .../maven/verifier/PluginUnitTest.java | 47 ++++++++++ .../basic-baseclass-from-mappings/pom.xml | 48 ++++++++++ .../contracts/com/hello/v1/Messaging.groovy | 37 ++++++++ .../contracts/com/hello/v1/Sample.groovy | 34 +++++++ .../contracts/com/hello/v1/WithList.groovy | 34 +++++++ .../basic-generated-baseclass/pom.xml | 42 +++++++++ .../contracts/hello/v1/Messaging.groovy | 37 ++++++++ .../contracts/hello/v1/Sample.groovy | 34 +++++++ .../contracts/hello/v1/WithList.groovy | 34 +++++++ .../contract/verifier/TestGenerator.groovy | 2 +- .../verifier/builder/ClassBuilder.groovy | 36 +++++++- .../builder/SingleTestGenerator.groovy | 12 +-- .../ContractVerifierConfigProperties.groovy | 22 +++++ .../verifier/GeneratorScannerSpec.groovy | 8 +- .../verifier/builder/ClassBuilderSpec.groovy | 55 +++++++++++ .../builder/SingleTestGeneratorSpec.groovy | 6 +- 33 files changed, 746 insertions(+), 33 deletions(-) rename samples/standalone/dsl/http-server/src/test/java/com/example/fraud/{MvcTest.java => FraudBase.java} (93%) rename samples/standalone/dsl/http-server/src/test/resources/contracts/{ => fraud}/shouldMarkClientAsFraud.groovy (100%) rename samples/standalone/dsl/http-server/src/test/resources/contracts/{ => fraud}/shouldMarkClientAsNotFraud.groovy (100%) create mode 100644 spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/BaseClassMapping.java create mode 100644 spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/pom.xml create mode 100644 spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/Messaging.groovy create mode 100644 spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/Sample.groovy create mode 100644 spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/WithList.groovy create mode 100644 spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/pom.xml create mode 100644 spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/Messaging.groovy create mode 100644 spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/Sample.groovy create mode 100644 spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/WithList.groovy create mode 100644 spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilderSpec.groovy diff --git a/docs/src/main/asciidoc/verifier/introduction.adoc b/docs/src/main/asciidoc/verifier/introduction.adoc index e1a365e737..afd6d9f768 100644 --- a/docs/src/main/asciidoc/verifier/introduction.adoc +++ b/docs/src/main/asciidoc/verifier/introduction.adoc @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/docs/src/main/asciidoc/verifier/rest.adoc b/docs/src/main/asciidoc/verifier/rest.adoc index 84c8692553..81de4b5b1c 100644 --- a/docs/src/main/asciidoc/verifier/rest.adoc +++ b/docs/src/main/asciidoc/verifier/rest.adoc @@ -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. diff --git a/samples/standalone/dsl/http-server/build.gradle b/samples/standalone/dsl/http-server/build.gradle index 166444ac52..045699f5b2 100644 --- a/samples/standalone/dsl/http-server/build.gradle +++ b/samples/standalone/dsl/http-server/build.gradle @@ -39,7 +39,7 @@ dependencyManagement { } contracts { - baseClassForTests = 'com.example.fraud.MvcTest' + packageWithBaseClasses = 'com.example.fraud' } dependencies { diff --git a/samples/standalone/dsl/http-server/pom.xml b/samples/standalone/dsl/http-server/pom.xml index 7e37c02efc..b072e18a47 100644 --- a/samples/standalone/dsl/http-server/pom.xml +++ b/samples/standalone/dsl/http-server/pom.xml @@ -76,7 +76,7 @@ ${spring-cloud-contract.version} true - com.example.fraud.MvcTest + com.example.fraud diff --git a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/MvcTest.java b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java similarity index 93% rename from samples/standalone/dsl/http-server/src/test/java/com/example/fraud/MvcTest.java rename to samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java index deb787523d..09bb85eb0e 100644 --- a/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/MvcTest.java +++ b/samples/standalone/dsl/http-server/src/test/java/com/example/fraud/FraudBase.java @@ -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() { diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/shouldMarkClientAsFraud.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.groovy similarity index 100% rename from samples/standalone/dsl/http-server/src/test/resources/contracts/shouldMarkClientAsFraud.groovy rename to samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsFraud.groovy diff --git a/samples/standalone/dsl/http-server/src/test/resources/contracts/shouldMarkClientAsNotFraud.groovy b/samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsNotFraud.groovy similarity index 100% rename from samples/standalone/dsl/http-server/src/test/resources/contracts/shouldMarkClientAsNotFraud.groovy rename to samples/standalone/dsl/http-server/src/test/resources/contracts/fraud/shouldMarkClientAsNotFraud.groovy diff --git a/samples/standalone/messaging/stream-source/build.gradle b/samples/standalone/messaging/stream-source/build.gradle index 16add1a93e..7c9856f0de 100644 --- a/samples/standalone/messaging/stream-source/build.gradle +++ b/samples/standalone/messaging/stream-source/build.gradle @@ -39,7 +39,9 @@ ext { } contracts { - baseClassForTests = 'com.example.source.SensorSourceTestBase' + baseClassMappings { + baseClassMapping('.*', 'com.example.source.SensorSourceTestBase') + } stubsOutputDir = stubsOutputDirRoot } diff --git a/samples/standalone/messaging/stream-source/pom.xml b/samples/standalone/messaging/stream-source/pom.xml index ec91aa6785..68095e442e 100644 --- a/samples/standalone/messaging/stream-source/pom.xml +++ b/samples/standalone/messaging/stream-source/pom.xml @@ -70,7 +70,12 @@ ${spring-cloud-contract.version} true - com.example.source.SensorSourceTestBase + + + .* + com.example.source.SensorSourceTestBase + + diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierExtension.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierExtension.groovy index 01afc18dc5..c0b5477b51 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierExtension.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierExtension.groovy @@ -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. + *

+ * Example of a mapping + *

+ * {@code .*.com.example.v1..*} -> {@code com.example.SomeBaseClass} + *

+ * 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 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 delegate + + BaseClassMapping(Map delegate) { + this.delegate = delegate + } + + void baseClassMapping(String packageRegex, String fqnBaseClass) { + this.delegate[packageRegex] = fqnBaseClass + } + + void baseClassMapping(Map mapping) { + this.delegate.putAll(mapping) + } + } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ExtensionToProperties.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ExtensionToProperties.groovy index 6e0262ebfe..83c6dca318 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ExtensionToProperties.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/main/groovy/org/springframework/cloud/contract/verifier/plugin/ExtensionToProperties.groovy @@ -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 ) } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierIntegrationSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierIntegrationSpec.groovy index ced185b132..75ceb96eef 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierIntegrationSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierIntegrationSpec.groovy @@ -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 diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierSpec.groovy b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierSpec.groovy index e9f6a3f0ad..8da257cb0d 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierSpec.groovy +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/groovy/org/springframework/cloud/contract/verifier/plugin/ContractVerifierSpec.groovy @@ -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 + } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle index 54a205d48e..ac13615895 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle +++ b/spring-cloud-contract-tools/spring-cloud-contract-gradle-plugin/src/test/resources/functionalTest/scenarioProject/build.gradle @@ -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 diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/BaseClassMapping.java b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/BaseClassMapping.java new file mode 100644 index 0000000000..dc72724789 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/BaseClassMapping.java @@ -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; + } +} diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/ConvertMojo.java b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/ConvertMojo.java index 006e59128c..df476d61b8 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/ConvertMojo.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/ConvertMojo.java @@ -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); diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/GenerateTestsMojo.java b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/GenerateTestsMojo.java index b041994013..fe0a1f7460 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/GenerateTestsMojo.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/main/java/org/springframework/cloud/contract/maven/verifier/GenerateTestsMojo.java @@ -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. + *

+ * Example of a mapping + *

+ * {@code .*.com.example.v1..*} -> {@code com.example.SomeBaseClass} + *

+ * 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 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 mappingsToMap() { + Map map = new HashMap<>(); + if (this.baseClassMappings == null) { + return map; + } + for (BaseClassMapping mapping : this.baseClassMappings) { + map.put(mapping.getContractPackageRegex(), mapping.getBaseClassFQN()); + } + return map; + } + public List getExcludedFiles() { return this.excludedFiles; } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/java/org/springframework/cloud/contract/maven/verifier/PluginUnitTest.java b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/java/org/springframework/cloud/contract/maven/verifier/PluginUnitTest.java index 4ecceb79b4..4889424662 100644 --- a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/java/org/springframework/cloud/contract/maven/verifier/PluginUnitTest.java +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/java/org/springframework/cloud/contract/maven/verifier/PluginUnitTest.java @@ -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"); + } } diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/pom.xml new file mode 100644 index 0000000000..87f1fc4425 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/pom.xml @@ -0,0 +1,48 @@ + + + + 4.0.0 + + org.springframework.cloud.verifier.sample + sample-project + 0.1 + + + + + + org.springframework.cloud + spring-cloud-contract-maven-plugin + + com.example.FooBase + + + .*com.* + com.example.TestBase + + + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/Messaging.groovy b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/Messaging.groovy new file mode 100644 index 0000000000..b21f448049 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/Messaging.groovy @@ -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') + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/Sample.groovy b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/Sample.groovy new file mode 100644 index 0000000000..094820225b --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/Sample.groovy @@ -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' + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/WithList.groovy b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/WithList.groovy new file mode 100644 index 0000000000..f542a19e9a --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-baseclass-from-mappings/src/test/resources/contracts/com/hello/v1/WithList.groovy @@ -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" ] }''' + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/pom.xml b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/pom.xml new file mode 100644 index 0000000000..dfdd1e3394 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/pom.xml @@ -0,0 +1,42 @@ + + + + 4.0.0 + + org.springframework.cloud.verifier.sample + sample-project + 0.1 + + + + + + org.springframework.cloud + spring-cloud-contract-maven-plugin + + hello + + + + + + + \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/Messaging.groovy b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/Messaging.groovy new file mode 100644 index 0000000000..b21f448049 --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/Messaging.groovy @@ -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') + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/Sample.groovy b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/Sample.groovy new file mode 100644 index 0000000000..094820225b --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/Sample.groovy @@ -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' + } + } +} \ No newline at end of file diff --git a/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/WithList.groovy b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/WithList.groovy new file mode 100644 index 0000000000..f542a19e9a --- /dev/null +++ b/spring-cloud-contract-tools/spring-cloud-contract-maven-plugin/src/test/projects/basic-generated-baseclass/src/test/resources/contracts/hello/v1/WithList.groovy @@ -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" ] }''' + } +} \ No newline at end of file diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy index 5457f654a7..c81afc29e8 100755 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/TestGenerator.groovy @@ -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() } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilder.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilder.groovy index 4552ca7f05..24d233bf55 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilder.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilder.groovy @@ -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 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 diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGenerator.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGenerator.groovy index 90ffd53a46..42437f9fed 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGenerator.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGenerator.groovy @@ -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 listOfFiles, String className, String classPackage) { - ClassBuilder clazz = createClass(capitalize(className), classPackage, configProperties) + String buildClass(Collection 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] } diff --git a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.groovy b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.groovy index 9529f50910..c4af73a851 100644 --- a/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.groovy +++ b/spring-cloud-contract-verifier/src/main/groovy/org/springframework/cloud/contract/verifier/config/ContractVerifierConfigProperties.groovy @@ -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. + *

+ * Example of a mapping + *

+ * {@code .*.com.example.v1..*} -> {@code com.example.SomeBaseClass} + *

+ * 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 baseClassMappings + } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/GeneratorScannerSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/GeneratorScannerSpec.groovy index 85f8c84fd4..0444d49ef0 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/GeneratorScannerSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/GeneratorScannerSpec.groovy @@ -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" } } diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilderSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilderSpec.groovy new file mode 100644 index 0000000000..2fcc6010dc --- /dev/null +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/ClassBuilderSpec.groovy @@ -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' + } + +} diff --git a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy index 8bb9f1591f..23d0e35430 100644 --- a/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy +++ b/spring-cloud-contract-verifier/src/test/groovy/org/springframework/cloud/contract/verifier/builder/SingleTestGeneratorSpec.groovy @@ -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) }