496 lines
18 KiB
Plaintext
496 lines
18 KiB
Plaintext
[[getting-started-first-application]]
|
|
= Developing Your First Spring Cloud Contract-based Application
|
|
|
|
include::partial$_attributes.adoc[]
|
|
|
|
This brief tour walks through using Spring Cloud Contract. It consists of the following topics:
|
|
|
|
* xref:getting-started/first-application.adoc#getting-started-first-application-producer[On the Producer Side]
|
|
* xref:getting-started/first-application.adoc#getting-started-first-application-consumer[On the Consumer Side]
|
|
|
|
You can find an even more brief tour
|
|
xref:getting-started/three-second-tour.adoc[here].
|
|
|
|
For the sake of this example, the `Stub Storage` is Nexus/Artifactory.
|
|
|
|
The following UML diagram shows the relationship of the parts of Spring Cloud Contract:
|
|
|
|
[plantuml, getting-started-three-second, png]
|
|
----
|
|
"API Producer"->"API Producer": add Spring Cloud \nContract (SCC) plugin
|
|
"API Producer"->"API Producer": add SCC Verifier dependency
|
|
"API Producer"->"API Producer": define contracts
|
|
"API Producer"->"Build": run build
|
|
"Build"->"SCC Plugin": generate \ntests, stubs and stubs \nartifact (e.g. stubs-jar)
|
|
"Build"->"Stub Storage": upload contracts \nand stubs and the project arifact
|
|
"Build"->"API Producer": Build successful
|
|
"API Consumer"->"API Consumer": add SCC Stub Runner \ndependency
|
|
"API Consumer"->"API Consumer": write a SCC Stub Runner \nbased contract test
|
|
"SCC Stub Runner"->"Stub Storage": test asks for [API Producer] stubs
|
|
"Stub Storage"->"SCC Stub Runner": fetch the [API Producer] stubs
|
|
"SCC Stub Runner"->"SCC Stub Runner": run in memory\n HTTP server stubs
|
|
"API Consumer"->"SCC Stub Runner": send a request \nto the HTTP server stub
|
|
"SCC Stub Runner"->"API Consumer": communication is correct
|
|
----
|
|
|
|
[[getting-started-first-application-producer]]
|
|
== On the Producer Side
|
|
|
|
To start working with `Spring Cloud Contract`, you can add the Spring Cloud Contract Verifier
|
|
dependency and plugin to your build file, as the following example shows:
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
include::{samples_path}/standalone/dsl/http-server/pom.xml[tags=verifier_test_dependencies,indent=0]
|
|
----
|
|
|
|
The following listing shows how to add the plugin, which should go in the build/plugins
|
|
portion of the file:
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
<plugin>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
|
|
<version>${spring-cloud-contract.version}</version>
|
|
<extensions>true</extensions>
|
|
</plugin>
|
|
----
|
|
|
|
[TIP]
|
|
====
|
|
The easiest way to get started is to go to https://start.spring.io[the Spring Initializr]
|
|
and add "`Web`" and "`Contract Verifier`" as dependencies. Doing so pulls in the previously
|
|
mentioned dependencies and everything else you need in the `pom.xml` file (except for
|
|
setting the base test class, which we cover later in this section). The following image
|
|
shows the settings to use in https://start.spring.io[the Spring Initializr]:
|
|
|
|
image::start_spring_io_dependencies.png[width=800,alt=Spring Initializr with Web and Contract Verifier]
|
|
====
|
|
|
|
Now you can add files with `REST/` messaging contracts
|
|
expressed in either Groovy DSL or YAML to the contracts directory, which is set by the
|
|
`contractsDslDir` property. By default, it is `$rootDir/src/test/resources/contracts`.
|
|
Note that the file name does not matter. You can organize your contracts within this
|
|
directory with whatever naming scheme you like.
|
|
|
|
For the HTTP stubs, a contract defines what kind of response should be returned for a
|
|
given request (taking into account the HTTP methods, URLs, headers, status codes, and so
|
|
on). The following example shows an HTTP stub contract in both Groovy and YAML:
|
|
|
|
[tabs]
|
|
======
|
|
groovy::
|
|
+
|
|
[source,groovy,indent=0,role="primary"]
|
|
----
|
|
package contracts
|
|
|
|
org.springframework.cloud.contract.spec.Contract.make {
|
|
request {
|
|
method 'PUT'
|
|
url '/fraudcheck'
|
|
body([
|
|
"client.id": $(regex('[0-9]{10}')),
|
|
loanAmount: 99999
|
|
])
|
|
headers {
|
|
contentType('application/json')
|
|
}
|
|
}
|
|
response {
|
|
status OK()
|
|
body([
|
|
fraudCheckStatus: "FRAUD",
|
|
"rejection.reason": "Amount too high"
|
|
])
|
|
headers {
|
|
contentType('application/json')
|
|
}
|
|
}
|
|
}
|
|
----
|
|
|
|
yaml::
|
|
+
|
|
[source,yaml,indent=0,role="secondary"]
|
|
----
|
|
request:
|
|
method: PUT
|
|
url: /fraudcheck
|
|
body:
|
|
"client.id": 1234567890
|
|
loanAmount: 99999
|
|
headers:
|
|
Content-Type: application/json
|
|
matchers:
|
|
body:
|
|
- path: $.['client.id']
|
|
type: by_regex
|
|
value: "[0-9]{10}"
|
|
response:
|
|
status: 200
|
|
body:
|
|
fraudCheckStatus: "FRAUD"
|
|
"rejection.reason": "Amount too high"
|
|
headers:
|
|
Content-Type: application/json;charset=UTF-8
|
|
----
|
|
======
|
|
|
|
If you need to use messaging, you can define:
|
|
|
|
* The input and output messages (taking into account from where it
|
|
was sent, the message body, and the header).
|
|
* The methods that should be called after the message is received.
|
|
* The methods that, when called, should trigger a message.
|
|
|
|
The following example shows a Camel messaging contract:
|
|
|
|
[tabs]
|
|
======
|
|
groovy::
|
|
+
|
|
[source,groovy,indent=0,role="primary"]
|
|
----
|
|
include::{verifier_root_path}/src/test/groovy/org/springframework/cloud/contract/verifier/builder/MessagingMethodBodyBuilderSpec.groovy[tags=trigger_method_dsl]
|
|
----
|
|
|
|
yaml::
|
|
+
|
|
[source,yaml,indent=0,role="secondary"]
|
|
----
|
|
include::{verifier_root_path}/src/test/resources/yml/contract_message_scenario1.yml[indent=0]
|
|
----
|
|
======
|
|
|
|
Running `./mvnw clean install` automatically generates tests that verify the application
|
|
compliance with the added contracts. By default, the generated tests are under
|
|
`org.springframework.cloud.contract.verifier.tests.`.
|
|
|
|
The generated tests may differ, depending on which framework and test type you have set up
|
|
in your plugin.
|
|
|
|
In the next listing, you can find:
|
|
|
|
- The default test mode for HTTP contracts in `MockMvc`
|
|
- A JAX-RS client with the `JAXRS` test mode
|
|
- A `WebTestClient`-based test (this is particularly recommended while working with
|
|
Reactive, `Web-Flux`-based applications) set with the `WEBTESTCLIENT` test mode
|
|
|
|
NOTE: You need only one of these test frameworks. MockMvc is the default. To use one
|
|
of the other frameworks, add its library to your classpath.
|
|
|
|
The following listing shows samples for all frameworks:
|
|
|
|
[tabs]
|
|
======
|
|
mockmvc::
|
|
+
|
|
[source,java,indent=0,role="primary"]
|
|
----
|
|
@Test
|
|
public void validate_shouldMarkClientAsFraud() throws Exception {
|
|
// given:
|
|
MockMvcRequestSpecification request = given()
|
|
.header("Content-Type", "application/vnd.fraud.v1+json")
|
|
.body("{\"client.id\":\"1234567890\",\"loanAmount\":99999}");
|
|
|
|
// when:
|
|
ResponseOptions response = given().spec(request)
|
|
.put("/fraudcheck");
|
|
|
|
// then:
|
|
assertThat(response.statusCode()).isEqualTo(200);
|
|
assertThat(response.header("Content-Type")).matches("application/vnd.fraud.v1.json.*");
|
|
// and:
|
|
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
|
|
assertThatJson(parsedJson).field("['fraudCheckStatus']").matches("[A-Z]{5}");
|
|
assertThatJson(parsedJson).field("['rejection.reason']").isEqualTo("Amount too high");
|
|
}
|
|
----
|
|
|
|
jaxrs::
|
|
+
|
|
[source,java,indent=0,role="secondary"]
|
|
----
|
|
@SuppressWarnings("rawtypes")
|
|
public class FooTest {
|
|
WebTarget webTarget;
|
|
|
|
@Test
|
|
public void validate_() throws Exception {
|
|
|
|
// when:
|
|
Response response = webTarget
|
|
.path("/users")
|
|
.queryParam("limit", "10")
|
|
.queryParam("offset", "20")
|
|
.queryParam("filter", "email")
|
|
.queryParam("sort", "name")
|
|
.queryParam("search", "55")
|
|
.queryParam("age", "99")
|
|
.queryParam("name", "Denis.Stepanov")
|
|
.queryParam("email", "bob@email.com")
|
|
.request()
|
|
.build("GET")
|
|
.invoke();
|
|
String responseAsString = response.readEntity(String.class);
|
|
|
|
// then:
|
|
assertThat(response.getStatus()).isEqualTo(200);
|
|
|
|
// and:
|
|
DocumentContext parsedJson = JsonPath.parse(responseAsString);
|
|
assertThatJson(parsedJson).field("['property1']").isEqualTo("a");
|
|
}
|
|
|
|
}
|
|
----
|
|
|
|
webtestclient::
|
|
+
|
|
[source,java,indent=0,role="secondary"]
|
|
----
|
|
@Test
|
|
public void validate_shouldRejectABeerIfTooYoung() throws Exception {
|
|
// given:
|
|
WebTestClientRequestSpecification request = given()
|
|
.header("Content-Type", "application/json")
|
|
.body("{\"age\":10}");
|
|
|
|
// when:
|
|
WebTestClientResponse response = given().spec(request)
|
|
.post("/check");
|
|
|
|
// then:
|
|
assertThat(response.statusCode()).isEqualTo(200);
|
|
assertThat(response.header("Content-Type")).matches("application/json.*");
|
|
// and:
|
|
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
|
|
assertThatJson(parsedJson).field("['status']").isEqualTo("NOT_OK");
|
|
}
|
|
----
|
|
======
|
|
|
|
As the implementation of the functionalities described by the contracts is not yet
|
|
present, the tests fail.
|
|
|
|
To make them pass, you must add the correct implementation of handling either HTTP
|
|
requests or messages. Also, you must add a base test class for auto-generated
|
|
tests to the project. This class is extended by all the auto-generated tests and should
|
|
contain all the setup necessary information needed to run them (for example,
|
|
`RestAssuredMockMvc` controller setup or messaging test setup).
|
|
|
|
The following example, from `pom.xml`, shows how to specify the base test class:
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
<build>
|
|
<plugins>
|
|
<plugin>
|
|
<groupId>org.springframework.cloud</groupId>
|
|
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
|
|
<version>2.1.2.RELEASE</version>
|
|
<extensions>true</extensions>
|
|
<configuration>
|
|
<baseClassForTests>com.example.contractTest.BaseTestClass</baseClassForTests> <1>
|
|
</configuration>
|
|
</plugin>
|
|
<plugin>
|
|
<groupId>org.springframework.boot</groupId>
|
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
|
</plugin>
|
|
</plugins>
|
|
</build>
|
|
----
|
|
<1> The `baseClassForTests` element lets you specify your base test class. It must be a child
|
|
of a `configuration` element within `spring-cloud-contract-maven-plugin`.
|
|
|
|
The following example shows a minimal (but functional) base test class:
|
|
|
|
[source,java, indent=0]
|
|
----
|
|
package com.example.contractTest;
|
|
|
|
import org.junit.Before;
|
|
|
|
import io.restassured.module.mockmvc.RestAssuredMockMvc;
|
|
|
|
public class BaseTestClass {
|
|
|
|
@Before
|
|
public void setup() {
|
|
RestAssuredMockMvc.standaloneSetup(new FraudController());
|
|
}
|
|
}
|
|
----
|
|
|
|
This minimal class really is all you need to get your tests to work. It serves as a
|
|
starting place to which the automatically generated tests attach.
|
|
|
|
Now we can move on to the implementation. For that, we first need a data class, which we
|
|
then use in our controller. The following listing shows the data class:
|
|
|
|
[source,java, indent=0]
|
|
----
|
|
package com.example.Test;
|
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
|
|
|
public class LoanRequest {
|
|
|
|
@JsonProperty("client.id")
|
|
private String clientId;
|
|
|
|
private Long loanAmount;
|
|
|
|
public String getClientId() {
|
|
return clientId;
|
|
}
|
|
|
|
public void setClientId(String clientId) {
|
|
this.clientId = clientId;
|
|
}
|
|
|
|
public Long getLoanAmount() {
|
|
return loanAmount;
|
|
}
|
|
|
|
public void setLoanRequestAmount(Long loanAmount) {
|
|
this.loanAmount = loanAmount;
|
|
}
|
|
}
|
|
----
|
|
|
|
The preceding class provides an object in which we can store the parameters. Because the
|
|
client ID in the contract is called `client.id`, we need to use the
|
|
`@JsonProperty("client.id")` parameter to map it to the `clientId` field.
|
|
|
|
Now we can move along to the controller, which the following listing shows:
|
|
|
|
[source,java, indent=0]
|
|
----
|
|
package com.example.docTest;
|
|
|
|
import org.springframework.web.bind.annotation.PutMapping;
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
@RestController
|
|
public class FraudController {
|
|
|
|
@PutMapping(value = "/fraudcheck", consumes="application/json", produces="application/json")
|
|
public String check(@RequestBody LoanRequest loanRequest) { <1>
|
|
|
|
if (loanRequest.getLoanAmount() > 10000) { <2>
|
|
return "{fraudCheckStatus: FRAUD, rejection.reason: Amount too high}"; <3>
|
|
} else {
|
|
return "{fraudCheckStatus: OK, acceptance.reason: Amount OK}"; <4>
|
|
}
|
|
}
|
|
}
|
|
----
|
|
<1> We map the incoming parameters to a `LoanRequest` object.
|
|
<2> We check the requested loan amount to see if it is too much.
|
|
<3> If it is too much, we return the JSON (created with a simple string here) that the
|
|
test expects.
|
|
<4> If we had a test to catch when the amount is allowable, we could match it to this output.
|
|
|
|
The `FraudController` is about as simple as things get. You can do much more, including
|
|
logging, validating the client ID, and so on.
|
|
|
|
Once the implementation and the test base class are in place, the tests pass, and both the
|
|
application and the stub artifacts are built and installed in the local Maven repository.
|
|
Information about installing the stubs jar to the local repository appears in the logs, as
|
|
the following example shows:
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
[INFO] --- spring-cloud-contract-maven-plugin:1.0.0.BUILD-SNAPSHOT:generateStubs (default-generateStubs) @ http-server ---
|
|
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar
|
|
[INFO]
|
|
[INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ http-server ---
|
|
[INFO] Building jar: /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar
|
|
[INFO]
|
|
[INFO] --- spring-boot-maven-plugin:1.5.5.BUILD-SNAPSHOT:repackage (default) @ http-server ---
|
|
[INFO]
|
|
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ http-server ---
|
|
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.jar
|
|
[INFO] Installing /some/path/http-server/pom.xml to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT.pom
|
|
[INFO] Installing /some/path/http-server/target/http-server-0.0.1-SNAPSHOT-stubs.jar to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
|
|
----
|
|
|
|
You can now merge the changes and publish both the application and the stub artifacts
|
|
in an online repository.
|
|
|
|
[[getting-started-first-application-consumer]]
|
|
== On the Consumer Side
|
|
|
|
You can use Spring Cloud Contract Stub Runner in the integration tests to get a running
|
|
WireMock instance or messaging route that simulates the actual service.
|
|
|
|
To get started, add the dependency to `Spring Cloud Contract Stub Runner`, as follows:
|
|
|
|
[source,xml,indent=0]
|
|
----
|
|
include::{samples_path}/standalone/dsl/http-client/pom.xml[tags=stub_runner,indent=0]
|
|
----
|
|
|
|
You can get the Producer-side stubs installed in your Maven repository in either of two
|
|
ways:
|
|
|
|
* By checking out the Producer side repository and adding contracts and generating the
|
|
stubs by running the following commands:
|
|
+
|
|
[source,bash,indent=0]
|
|
----
|
|
$ cd local-http-server-repo
|
|
$ ./mvnw clean install -DskipTests
|
|
----
|
|
NOTE: The tests are skipped because the Producer-side contract implementation is not yet
|
|
in place, so the automatically-generated contract tests fail.
|
|
|
|
* By getting existing producer service stubs from a remote repository. To do so,
|
|
pass the stub artifact IDs and artifact repository URL as `Spring Cloud Contract Stub
|
|
Runner` properties, as the following example shows:
|
|
+
|
|
[source,yaml,indent=0]
|
|
----
|
|
include::{samples_path}/standalone/dsl/http-client/src/test/resources/application-test-repo.yaml[]
|
|
----
|
|
|
|
Now you can annotate your test class with `@AutoConfigureStubRunner`. In the annotation,
|
|
provide the `group-id` and `artifact-id` for `Spring Cloud Contract Stub Runner` to run
|
|
the collaborators' stubs for you, as the following example shows:
|
|
|
|
[source,java, indent=0]
|
|
----
|
|
@RunWith(SpringRunner.class)
|
|
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
|
|
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
|
|
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
|
|
public class LoanApplicationServiceTests {
|
|
. . .
|
|
}
|
|
----
|
|
|
|
TIP: Use the `REMOTE` `stubsMode` when downloading stubs from an online repository and
|
|
`LOCAL` for offline work.
|
|
|
|
In your integration test, you can receive stubbed versions of HTTP responses or messages
|
|
that are expected to be emitted by the collaborator service. You can see entries similar
|
|
to the following in the build logs:
|
|
|
|
[source,bash,indent=0]
|
|
----
|
|
2016-07-19 14:22:25.403 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Desired version is + - will try to resolve the latest version
|
|
2016-07-19 14:22:25.438 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved version is 0.0.1-SNAPSHOT
|
|
2016-07-19 14:22:25.439 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolving artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT using remote repositories []
|
|
2016-07-19 14:22:25.451 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Resolved artifact com.example:http-server:jar:stubs:0.0.1-SNAPSHOT to /path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar
|
|
2016-07-19 14:22:25.465 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacking stub from JAR [URI: file:/path/to/your/.m2/repository/com/example/http-server/0.0.1-SNAPSHOT/http-server-0.0.1-SNAPSHOT-stubs.jar]
|
|
2016-07-19 14:22:25.475 INFO 41050 --- [ main] o.s.c.c.stubrunner.AetherStubDownloader : Unpacked file to [/var/folders/0p/xwq47sq106x1_g3dtv6qfm940000gq/T/contracts100276532569594265]
|
|
2016-07-19 14:22:27.737 INFO 41050 --- [ main] o.s.c.c.stubrunner.StubRunnerExecutor : All stubs are now running RunningStubs [namesAndPorts={com.example:http-server:0.0.1-SNAPSHOT:stubs=8080}]
|
|
----
|
|
|