From 48ea06b66a6a4a783fbd51ecaed7b5ad38b1b631 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 24 Jul 2019 13:16:29 +0200 Subject: [PATCH] Added flows --- docs/src/main/asciidoc/using.adoc | 358 +++++++++++++++++++++++++++++- 1 file changed, 355 insertions(+), 3 deletions(-) diff --git a/docs/src/main/asciidoc/using.adoc b/docs/src/main/asciidoc/using.adoc index 8aac5d4a88..07ddd2d03e 100644 --- a/docs/src/main/asciidoc/using.adoc +++ b/docs/src/main/asciidoc/using.adoc @@ -412,14 +412,366 @@ You can check the < ctx.result("Hello World")); + } + + public Javalin run(int port) { + return registerGet(start(port)); + } + +} +---- + +Setup the plugin to use the `EXPLICIT` mode (i.e. to send out requests to a real port). + +==== +[source,xml,indent=0,role="primary"] +.maven +---- + + org.springframework.cloud + spring-cloud-contract-maven-plugin + ${spring-cloud-contract.version} + true + + com.example.demo.BaseClass + + EXPLICIT + + +---- + +[source,groovy,indent=0,role="secondary"] +.gradle +---- +contracts { + // This will setup the EXPLICIT mode for the tests + testMode = "EXPLICIT" + baseClassForTests = "com.example.demo.BaseClass" +} +---- +==== + +The base class could look like this + +[source,java,indent=0] +---- +import io.javalin.Javalin; +import io.restassured.RestAssured; +import org.junit.After; +import org.junit.Before; +import org.springframework.util.SocketUtils; + +public class BaseClass { + + Javalin app; + + @Before + public void setup() { + // pick a random port + int port = SocketUtils.findAvailableTcpPort(); + // start the application at a random port + this.app = start(port); + // tell Rest Assured where the started application is + RestAssured.baseURI = "http://localhost:" + port; + } + + @After + public void close() { + // stop the server after each test + this.app.stop(); + } + + private Javalin start(int port) { + // reuse the production logic to start a server + return new DemoApplication().run(port); + } +} +---- + +With such a setup + +* we've setup the Spring Cloud Contract plugin that it uses the `EXPLICIT` mode to send real requests instead of mocked ones +* we've defined a base class that +** starts the HTTP server on a random port for each test +** sets Rest Assured to send requests to that port +** closes the HTTP server after each test [[flows-provider-non-jvm]] == Provider contract testing with stubs in Artifactory in non JVM world -[[flows-provider-rest-docs]] -== Provider contract testing with REST Docs and stubs in Artifactory +In this flow we assume that +* the API Producer and API Consumer are non JVM applications. +* the contract definitions are written in YAML +* the Stub Storage is Artifactory / Nexus +* Spring Cloud Contract Docker (SCC Docker) and Spring Cloud Contract Stub Runner Docker (SCC Stub Runner Docker) images will be used + +You can read more about how to use Spring Cloud Contract with Docker <>. + +Over https://spring.io/blog/2018/02/13/spring-cloud-contract-in-a-polyglot-world[here] you can read a blog post about how to use Spring Cloud Contract in a polyglot world. + +https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs/[Here] you can find a sample of a NodeJS application using Spring Cloud Contract both as a producer and a consumer. + +[[flows-provider-non-jvm-producer]] +=== Producer Flow + +At a high level the producer + +* writes contract definitions e.g. in YAML +* sets up the build tool to +** start the application with mocked services on a given port X +** in case when mocking is not possible you can setup the infrastructure and define tests in a stateful way +** run the Spring Cloud Contract Docker image and pass the port of a running application as an environment variable +* The SCC Docker image +** generates the tests from the attached volume +** runs the tests against the running application +** upon test completion, stubs will get uploaded to a Stub Storage (e.g. Artifactory, Git) + +[plantuml, flows-provider-non-jvm-producer, png] +---- +"API Producer"->"API Producer": write contract definitions +"API Producer"->"API Producer": (preferable) prepare a way\nto run the app\nwith mocked services +"API Producer"->"API Producer\nbuild": run the build +"API Producer\nbuild"->"API Producer\nrunning app": run the app\non port X\nwith mocked services +"API Producer\nbuild"->"SCC Docker": attach contract definitions\nas a volume +"API Producer\nbuild"->"SCC Docker": set environment variables\ne.g. app running on port X +"API Producer\nbuild"->"SCC Docker": run the contract tests +"SCC Docker"->"SCC Docker\nimage": run the contract tests +"SCC Docker\nimage"->"SCC Docker\nimage": pick the contract definitions\nfrom volume +"SCC Docker\nimage"->"SCC Docker\nimage": generate contract tests +"SCC Docker\nimage"->"SCC Docker\nimage": run the tests\nagainst app running\non port X +"SCC Docker\nimage"->"SCC Docker\nimage": the tests are passing! +"SCC Docker\nimage"->"Stub Storage": upload the stubs +"SCC Docker\nimage"->"SCC Docker": build successful +"SCC Docker"->"API Producer\nbuild": build successful +"API Producer\nbuild"->"API Producer": build successful +---- + +[[flows-provider-non-jvm-consumer]] +=== Consumer Flow + +At a high level the consumer + +* sets up the build tool to +** start the Spring Cloud Contract Stub Runner Docker image and start the stubs +*** the environment variables passed will configure +**** the stubs to fetch +**** location of the repositories +*** to use the local storage you can also attach it as a volume +*** the ports at which the stubs are running need to be exposed +** run your application tests against the running stubs + +[plantuml, flows-provider-non-jvm-consumer, png] +---- +"API Consumer"->"API Consumer\nbuild": run the build +"API Consumer\nbuild"->"SCC\nStub Runner\nDocker": set environment variables\ne.g. stub X running on port Y +"SCC\nStub Runner\nDocker"->"SCC\nStub Runner\nDocker\nimage": fetch and run\nthe stubs +"SCC\nStub Runner\nDocker\nimage"->"Stub Storage": fetch the stubs of X +"Stub Storage"->"SCC\nStub Runner\nDocker\nimage": stubs found +"SCC\nStub Runner\nDocker\nimage"->"X Stub": run the stub of X +"X Stub"->"SCC\nStub Runner\nDocker\nimage": stub is running\non port Y +"SCC\nStub Runner\nDocker\nimage"->"SCC\nStub Runner\nDocker": stubs running and\nready for tests +"API Consumer\nbuild"->"API Consumer\nbuild": run tests against X stub +"API Consumer\nbuild"->"X Stub": send a request +"X Stub"->"API Consumer\nbuild": response received +"API Consumer\nbuild"->"API Consumer": build successful +---- + +[[flows-provider-rest-docs]] +== Provider contract testing with REST Docs and stubs in Nexus / Artifactory + +In this flow, we will not use a Spring Cloud Contract Plugin to generate tests and stubs. We will write https://spring.io/projects/spring-restdocs[Spring RESTDocs] and from them we will automatically generate stubs. Finally, we will setup our builds to package the stubs and upload them to the Stub Storage - in our case Nexus / Artifactory. + +You can also check the https://cloud-samples.spring.io/spring-cloud-contract-samples/tutorials/rest_docs.html[workshop page] for a step by step instruction on how to do this flow. + +[[flows-provider-rest-docs-producer]] +=== Producer flow + +As a producer + +* we write RESTDocs tests of our API +* we add Spring Cloud Contract Stub Runner starter to our build (`spring-cloud-starter-contract-stub-runner`) ++ +==== +[source,xml,indent=0,role="primary"] +.maven +---- + + + org.springframework.cloud + spring-cloud-starter-contract-stub-runner + test + + + + + + + org.springframework.cloud + spring-cloud-dependencies + ${spring-cloud.version} + pom + import + + + +---- + +[source,groovy,indent=0,role="secondary"] +.gradle +---- +dependencies { + testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} +---- +==== +* we setup the build tool to package our stubs ++ +==== +[source,xml,indent=0,role="primary"] +.maven +---- + + + + org.apache.maven.plugins + maven-assembly-plugin + + + stub + prepare-package + + single + + false + + true + + ${basedir}/src/assembly/stub.xml + + + + + + + + + + stubs + + jar + + false + + + ${project.build.directory}/generated-snippets/stubs + META-INF/${project.groupId}/${project.artifactId}/${project.version}/mappings + + **/* + + + + +---- + +[source,groovy,indent=0,role="secondary"] +.gradle +---- +task stubsJar(type: Jar) { + classifier = "stubs" + into("META-INF/${project.group}/${project.name}/${project.version}/mappings") { + include('**/*.*') + from("${project.buildDir}/generated-snippets/stubs") + } +} +// we need the tests to pass to build the stub jar +stubsJar.dependsOn(test) +bootJar.dependsOn(stubsJar) +---- +==== +* now, when we run the tests, stubs will be automatically published and packaged + +[plantuml, flows-provider-rest-docs-producer, png] +---- +"API Producer"->"API Producer": write RESTDocs tests +"API Producer"->"API Producer": add the stub runner\nstarter dependency +"API Producer"->"API Producer": setup the build tool to package\nthe generated stubs +"API Producer"->"API Producer\nbuild": run the build +"API Producer\nbuild"->"RESTDocs": generate HTTP snippets +"RESTDocs"->"Spring Cloud\nContract": generate HTTP stubs +"RESTDocs"->"Spring Cloud\nContract": (optional) generate\ncontract DSLs +"Spring Cloud\nContract"->"RESTDocs": files generated +"RESTDocs"->"API Producer\nbuild": snippets generated +"API Producer\nbuild"->"API Producer\nbuild": tests passed +"API Producer\nbuild"->"API Producer\nbuild": generate stubs jar +"API Producer\nbuild"->"Stub Storage": upload JAR with the application +"API Producer\nbuild"->"Stub Storage": upload JAR with the stubs +"Stub Storage"->"API Producer\nbuild": JARs uploaded +"API Producer\nbuild"->"API Producer": build successful +---- + +[[flows-provider-rest-docs-consumer]] +=== Consumer flow + +Since the consumer flow isn't affected by the tool used to generate the stubs, you can check the <> link to see the consumer side of the provider contract testing with stubs in Nexus / Artifactory flow. [[using-whats-next]] == What to Read Next