Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Continuous Integration With Jenkins, Artifactory, and Spring Cloud Contract

DZone's Guide to

Continuous Integration With Jenkins, Artifactory, and Spring Cloud Contract

Learn how you can use Jenkins to test and verify integration between your applications, especially in a system of microservices.

· DevOps Zone ·
Free Resource

Easily enforce open source policies in real time and reduce MTTRs from six weeks to six seconds with the Sonatype Nexus Platform. See for yourself - Free Vulnerability Scanner. 

Contract Driven Contract (CDC) testing is a method that allows you to verify integration between applications within your system. The number of such interactions may be really large especially if you maintain microservices-based architecture. Assuming that every microservice is developed by different teams or sometimes even different vendors, it is important to automate the whole testing process. As usual, we can use the Jenkins server for running contract tests within our Continuous Integration (CI) process.

The sample scenario has been visualized on the picture below. We have one application (person-service) that exposes API leveraged by three different applications. Each application is implementing by a different development team. Consequently, every application is stored in a separated Git repository and has a dedicated pipeline in Jenkins for building, testing, and deploying.

The source code of sample applications is available on GitHub in the repository sample-spring-cloud-contract-ci. I placed all the sample microservices in the single Git repository only for our demo simplification. We will still treat them as a separated microservices, developed and built independently.

In this article, I used Spring Cloud Contract for CDC implementation. It is the first choice solution for JVM applications written in Spring Boot. Contracts can be defined using Groovy or YAML notation. After building on the producer side, Spring Cloud Contract generates a special JAR file with the stubs suffix, which contains all defined contracts and JSON mappings. Such a JAR file can be built on Jenkins and then published on Artifactory. The contract consumer also uses the same Artifactory server, so they can use the latest version of the stubs file. Because every application expects a different response from person-service, we have to define three different contracts between person-service and a target consumer.

Let's analyze the sample scenario. Assuming we have performed some changes in the API exposed by person-service and modified contracts on the producer side, we would like to publish them on the shared server. First, we need to verify contracts against the producer (1), and in case of success, publish the artifact with stubs to Artifactory (2). All the pipelines defined for applications that use this contract are able to trigger the build on a new version of the JAR file with stubs (3). Then, the newest version of the contract is verified against the consumer (4). If contract testing fails, the pipeline is able to notify the responsible team about this failure.

1. Pre-Requirements

Before implementing and running any sample, we need to prepare our environment. We need to launch Jenkins and Artifactory servers on the local machine. The most suitable way for this is through Docker containers. Here are the commands required to run these containers.

$ docker run --name artifactory -d -p 8081:8081 docker.bintray.io/jfrog/artifactory-oss:latest
$ docker run --name jenkins -d -p 8080:8080 -p 50000:50000 jenkins/jenkins:lts

I don't know if you are familiar with tools like Artifactory and Jenkins, but after starting them, we need to configure some things. First, you need to initialize Maven repositories for Artifactory. You will be prompted for that just after the first launch. It also automatically adds one remote repository: JCenter Bintray, which is enough for our build. Jenkins also comes with a default set of plugins, which you can install just after the first launch (Install suggested plugins). For this demo, you will also have to install the plugin for integration with Artifactory. If you need more details about Jenkins and Artifactory configuration, you can refer to my older article, How to setup Continuous Delivery environment.

2. Building Contracts

We are beginning contract definition from the producer side application. Producer exposes only one GET /persons/{id} method that returns Person object. Here are the fields contained by Person class.

public class Person {

 private Integer id;
 private String firstName;
 private String lastName;
 @JsonFormat(pattern = "yyyy-MM-dd")
 private Date birthDate;
 private Gender gender;
 private Contact contact;
 private Address address;
 private String accountNo;

 // ...
}

The following picture illustrates which fields of the Person object are used by consumers. As you see, some of the fields are shared between consumers, while others are required only by a single consuming application.

Now we can take a look at the contract definition between person-service and bank-service.

import org.springframework.cloud.contract.spec.Contract

Contract.make {
 request {
  method 'GET'
  urlPath('/persons/1')
 }
 response {
  status OK()
  body([
   id: 1,
   firstName: 'Piotr',
   lastName: 'Minkowski',
   gender: $(regex('(MALE|FEMALE)')),
   contact: ([
    email: $(regex(email())),
    phoneNo: $(regex('[0-9]{9}$'))
   ])
  ])
  headers {
   contentType(applicationJson())
  }
 }
}

For comparison, here's the definition of a contract between person-service and letter-service.

import org.springframework.cloud.contract.spec.Contract

Contract.make {
 request {
  method 'GET'
  urlPath('/persons/1')
 }
 response {
  status OK()
  body([
   id: 1,
   firstName: 'Piotr',
   lastName: 'Minkowski',
   address: ([
    city: $(regex(alphaNumeric())),
    country: $(regex(alphaNumeric())),
    postalCode: $(regex('[0-9]{2}-[0-9]{3}')),
    houseNo: $(regex(positiveInt())),
    street: $(regex(nonEmpty()))
   ])
  ])
  headers {
   contentType(applicationJson())
  }
 }
}

3. Implementing Tests on the Producer Side

We have three different contracts assigned to the single endpoint exposed by person-service. We need to publish them in such a way that they are easily available for consumers. In that case, Spring Cloud Contract comes with a handy solution. We may define contracts with a different response for the same request, then choose the appropriate definition on the consumer side. All those contract definitions will be published within the same JAR file. Because we have three consumers we define three different contracts placed in the directories bank-consumer, contact-consumer and letter-consumer.

All the contracts will use a single base test class. To achieve this, we need to provide a fully qualified name of that class for Spring Cloud Contract Verifier plugin in pom.xml.

<plugin>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-contract-maven-plugin</artifactId>
	<extensions>true</extensions>
	<configuration>
		<baseClassForTests>pl.piomin.services.person.BasePersonContractTest</baseClassForTests>
	</configuration>
</plugin>

Here's the full definition of the base class for our contract tests. We will mock the repository bean with the answer matching the rules created inside the contract files.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.DEFINED_PORT)
public abstract class BasePersonContractTest {

 @Autowired
 WebApplicationContext context;
 @MockBean
 PersonRepository repository;

 @Before
 public void setup() {
  RestAssuredMockMvc.webAppContextSetup(this.context);
  PersonBuilder builder = new PersonBuilder()
   .withId(1)
   .withFirstName("Piotr")
   .withLastName("Minkowski")
   .withBirthDate(new Date())
   .withAccountNo("1234567890")
   .withGender(Gender.MALE)
   .withPhoneNo("500070935")
   .withCity("Warsaw")
   .withCountry("Poland")
   .withHouseNo(200)
   .withStreet("Al. Jerozolimskie")
   .withEmail("piotr.minkowski@gmail.com")
   .withPostalCode("02-660");
  when(repository.findById(1)).thenReturn(builder.build());
 }

}

The Spring Cloud Contract Maven plugin visible above is responsible for generating stubs from contract definitions. It is executed during Maven builds after running mvn clean install. The build is performed on Jenkins CI. The Jenkins pipeline is responsible for updating the remote Git repository, building binaries from source code, running automated tests, and finally, publishing the JAR file containing stubs on a remote artifact repository — Artifactory. Here's the Jenkins pipeline created for the contract producer side (person-service).

node {
  withMaven(maven:'M3') {
    stage('Checkout') {
      git url: 'https://github.com/piomin/sample-spring-cloud-contract-ci.git', credentialsId: 'piomin-github', branch: 'master'
    }
    stage('Publish') {
      def server = Artifactory.server 'artifactory'
      def rtMaven = Artifactory.newMavenBuild()
      rtMaven.tool = 'M3'
      rtMaven.resolver server: server, releaseRepo: 'libs-release', snapshotRepo: 'libs-snapshot'
      rtMaven.deployer server: server, releaseRepo: 'libs-release-local', snapshotRepo: 'libs-snapshot-local'
      rtMaven.deployer.artifactDeploymentPatterns.addInclude("*stubs*")
      def buildInfo = rtMaven.run pom: 'person-service/pom.xml', goals: 'clean install'
      rtMaven.deployer.deployArtifacts buildInfo
      server.publishBuildInfo buildInfo
    }
  }
}

We also need to include the dependency spring-cloud-starter-contract-verifier to the producer app to enable Spring Cloud Contract Verifier.

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-verifier</artifactId>
	<scope>test</scope>
</dependency>

4. Implementing Tests on the Consumer Side

To enable Spring Cloud Contract on the consumer side, we need to include the artifact spring-cloud-starter-contract-stub-runner to the project dependencies.

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
	<scope>test</scope>
</dependency>

The only thing left is to build a JUnit test, which verifies our contract by calling it through the OpenFeign client. The configuration of that test is provided inside the annotation @AutoConfigureStubRunner. We select the latest version of the person-service stubs artifact by setting + in the version section of the ids parameter. Because we have multiple contracts defined in person-service, we need to choose the right one for the current service by setting the consumer-name parameter. All the contract definitions are downloaded from the Artifactory server, so we set the stubsMode parameter to REMOTE. The address of the Artifactory server has to be set using the repositoryRoot property.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {
 "pl.piomin.services:person-service:+:stubs:8090"
}, consumerName = "letter-consumer", stubsPerConsumer = true, stubsMode = StubsMode.REMOTE, repositoryRoot = "http://192.168.99.100:8081/artifactory/libs-snapshot-local")
@DirtiesContext
public class PersonConsumerContractTest {

 @Autowired
 private PersonClient personClient;

 @Test
 public void verifyPerson() {
  Person p = personClient.findPersonById(1);
  Assert.assertNotNull(p);
  Assert.assertEquals(1, p.getId().intValue());
  Assert.assertNotNull(p.getFirstName());
  Assert.assertNotNull(p.getLastName());
  Assert.assertNotNull(p.getAddress());
  Assert.assertNotNull(p.getAddress().getCity());
  Assert.assertNotNull(p.getAddress().getCountry());
  Assert.assertNotNull(p.getAddress().getPostalCode());
  Assert.assertNotNull(p.getAddress().getStreet());
  Assert.assertNotEquals(0, p.getAddress().getHouseNo());
 }

}

Here's the Feign client implementation responsible for calling the endpoint exposed by person-service

@FeignClient("person-service")
public interface PersonClient {

 @GetMapping("/persons/{id}")
 Person findPersonById(@PathVariable("id") Integer id);

}

5. Setup of the Continuous Integration Process

We have already defined all the contracts required for our exercise. We have also built a pipeline responsible for building and publishing stubs with contracts on the producer side (person-service). It always publishes the newest version of stubs generated from the source code. Now, our goal is to launch pipelines defined for three consumer applications each time new stubs are published to the Artifactory server by the producer pipeline.

The best solution for this is to trigger a Jenkins build when you deploy an artifact. To achieve this, we use a Jenkins plugin called URLTrigger, which can be configured to watch for changes on a certain URL, in case a REST API endpoint is exposed by Artifactory for the selected repository path.

After installing URLTrigger, we have to enable it for all consumer pipelines. You can configure it to watch for changes in the returned JSON file from the Artifactory File List REST API, accessed via the URI http://192.168.99.100:8081/artifactory/api/storage/%5BPATH_TO_FOLDER_OR_REPO%5D/. The file maven-metadata.xml will change every time you deploy a new version of the application to Artifactory. We can monitor the change of the response's content between the last two polls. The last field that has to be filled is Schedule. If you set it to * * * * *, it will poll for a change every minute.

Our three pipelines for consumer applications are ready. The first run finished with success.

If you have already built the person-service application and publish stubs to Artifactory, you will see the following structure in the libs-snapshot-local repository. I have deployed three different versions of the API exposed by person-service. Each time I publish a new version of the contract, all the dependent pipelines are triggered to verify it.

The JAR file with contracts is published under the classifier stubs.

Spring Cloud Contract Stub Runner tries to find the latest version of contracts.

2018-07-04 11:46:53.273  INFO 4185 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is [+] - will try to resolve the latest version
2018-07-04 11:46:54.752  INFO 4185 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is [1.3-SNAPSHOT]
2018-07-04 11:46:54.823  INFO 4185 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact [pl.piomin.services:person-service:jar:stubs:1.3-SNAPSHOT] to /var/jenkins_home/.m2/repository/pl/piomin/services/person-service/1.3-SNAPSHOT/person-service-1.3-SNAPSHOT-stubs.jar

6. Testing a Change in the Contract

We have already prepared contracts and configured our CI environment. Now, let's perform a change in the API exposed by person-service. We will just change the name of one field: accountNo to accountNumber.

This change requires a change in the contract definition created on the producer side. If you modify the field name there, person-service will build successfully and a new version of the contract will be published to Artifactory. Because all other pipelines listen for changes in the latest version of JAR files with stubs, the build will be started automatically. The microservices letter-service and contact-service do not use the field accountNo, so their pipelines will not fail. Only the bank-service pipeline reports an error in the contract, as shown on the picture below.

Now, if you were notified about the failed verification of the newest contract version between person-service and bank-service, you can perform the required change on the consumer side.

Automate open source governance at scale across the entire software supply chain with the Nexus Platform. Learn more.

Topics:
devops ,continuous integration ,jenkins ,ci/cd ,artifactory ,spring cloud ,tutorial

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}