Files
spring-cloud-contract/docs/modules/ROOT/pages/getting-started/first-application.adoc
Marcin Grzejszczak 43318b0b05 Done
2023-09-11 19:00:22 +02:00

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}]
----