Server-Side CCD

In this section we will look at Contract Driven Development using Spring Cloud Contract (SCC) from the perspective of a (Java Spring) server side developer. We will see how to configure a project to read in contracts from a shared repository and have them executed against our api to make sure it is meeting the defined behavior in the contracts.

Configuring the Spring Cloud Contract Plugin

For SCC to validate that your API is matching the behaviors defined in the contracts we wrote in the previous step we must configure its plugin. The plugin has largely already been defined in the pom.xml of the service application, let's step through it below as well as what needs to be changed:

<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration>
<baseClassForTests>com.ibm.developer.produce.ContractsBaseClass</baseClassForTests>
<contractsRepositoryUrl>git://https://github.com/wkorando/produce-contracts.git</contractsRepositoryUrl>
<contractDependency>
<groupId>${project.groupId}</groupId>
<artifactId>${project.artifactId}</artifactId>
<version>${project.version}</version>
</contractDependency>
<contractsMode>LOCAL</contractsMode>
</configuration>
</plugin>

baseClassForTests Defines the class Spring Cloud Contract will use when executing the contracts. More on that in the next section.

contractsRepositoryUrl Defines the repository where the contracts are located. You will want to change this to fork you created from the previous page.

contractDependency These are the maven coordinates for where the contracts are stored as well as where the stubs artifact will be placed.

contractsMode This is a bit confusing, but SCC will clone the git contracts repo, which means the contracts will be available locally. After clone the repo, we need to tell SCC to then look for the contracts locally.

Contract Base Class

Full class including imports can be seen here.

As SCC is running the contracts against your API, you will need to have your API simulate what it would be doing as though it where actually running. This is where the Base Class comes into play. In the below base class I have created some mocks that simulate the expected behavior in the contracts.

For example, when findAllProduce is executed a call to GET:api/v1/produce is made which references this method in ProduceController:

@GetMapping
public ResponseEntity<Iterable<Produce>> findAllProduce() {
return ResponseEntity.ok(repo.findAll());
}

In ContractsBaseClass I have a mock that will the return all the produce items the contract is expecting:

when(repo.findAll()).thenReturn(
Arrays.asList(new Produce(1, "Apple", "Granny Smith", 100),
new Produce(2, "Apple", "Gala", 50),//
new Produce(3, "Corn", "Sweet", 1000), //
new Produce(4, "Pineapple", "", 300)));

Here is what the entire ContractsBaseClass looks like:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
public class ContractsBaseClass {
@Rule
public JUnitRestDocumentation restDocumentation = new JUnitRestDocumentation();
@Rule
public TestName testName = new TestName();
@Autowired
private ProduceController controller;
@MockBean
private ProduceRepo repo;
@MockBean
private ProduceService service;
@Before
public void before() throws Throwable {
when(repo.findAll()).thenReturn(
Arrays.asList(new Produce(1, "Apple", "Granny Smith", 100),
new Produce(2, "Apple", "Gala", 50),//
new Produce(3, "Corn", "Sweet", 1000), //
new Produce(4, "Pineapple", "", 300)));
when(service.findProduceByName("Apple")).thenReturn(
Arrays.asList(new Produce(1, "Apple", "Granny Smith", 100),
new Produce(2, "Apple", "Gala", 50)));
when(service.findProduceByName("+")).thenThrow(new ClientException("Produce name must be alpha numeric!"));
when(service.addNewProduce(new Produce(0, "Kiwi", "", 75))).thenReturn(new Produce(10, "Kiwi", "", 75));
when(service.addNewProduce(new Produce(0, "", "", 75))).thenThrow(new ClientException("Missing required value!"));
RestAssuredMockMvc.standaloneSetup(MockMvcBuilders
.standaloneSetup(controller)
.apply(documentationConfiguration(this.restDocumentation))
.alwaysDo(document(getClass().getSimpleName() + "_" + testName.getMethodName())));
}
}

Generating Documentation

SCC not only verifies your API matches the behavior defined in the contracts, it can also document your API as well using Spring REST Docs. To do this you will need to confgiure your build file like below:

<plugin>
<groupId>org.asciidoctor</groupId>
<artifactId>asciidoctor-maven-plugin</artifactId>
<version>1.5.3</version>
<executions>
<execution>
<id>generate-docs</id>
<phase>prepare-package</phase>
<goals>
<goal>process-asciidoc</goal>
</goals>
<configuration>
<backend>html</backend>
<doctype>book</doctype>
</configuration>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.springframework.restdocs</groupId>
<artifactId>spring-restdocs-asciidoctor</artifactId>
<version>${spring-restdocs.version}</version>
</dependency>
</dependencies>
</plugin>

Additionally in a base class you will need to add the JUnitRestDocumentation JUnit rule and use RestAssuredMockMvc like in the example above:

RestAssuredMockMvc.standaloneSetup(MockMvcBuilders
.standaloneSetup(controller)
.apply(documentationConfiguration(this.restDocumentation))
.alwaysDo(document(getClass().getSimpleName() + "_" + testName.getMethodName())));

Finally you will need to add a source folder src/main/asciidoc and create an index.adoc which defines what your documentation looks like. Produce Service already has one created and it looks like this:

= Produce Service API Guide
Billy Korando;
:doctype: book
:icons: font
:source-highlighter: highlightjs
:toc: left
:toclevels: 4
:sectlinks:
:operation-curl-request-title: Example request
:operation-http-response-title: Example response
[[overview]]
= Overview
API of Produce Service. Example of using contracts to both test an API to make sure it meets contract requirements and then generate API documentation from supplied contracts.
Contracts can be viewed here: https://github.com/wkorando/produce-contracts[Produce API Contracts]
[[resources-tag-retrieve]]
== Find all produce
A `GET` request to retrieve all produce items
operation::ContractsTest_validate_find_all_produce[snippets='curl-request,response-body,http-request,http-response']
[[resources-tag-retrieve]]
== Find all produce whose primary name matches Path value
A `GET` request to retrieve all produce items that match primary name
operation::ContractsTest_validate_find_produce_by_name[snippets='curl-request,response-body,http-request,http-response']
[[resources-tag-retrieve]]
== Add new produce item
A `POST` to add a new produce item
operation::ContractsTest_validate_add_produce[snippets='curl-request,request-body,response-body,http-request,http-response']
[[resources-tag-retrieve]]
== Failed add new produce item
A `POST` of client attempting to add new produce item but not supplying all required fields.
operation::ContractsTest_validate_add_produce_bad_client_data[snippets='curl-request,request-body,response-body,http-request,http-response']

Executing mvn package will generate an API document under:target/generated-docs/index.html