Continuous Integration with Jenkins, Artifactory and Spring Cloud Contract

Continuous Integration with Jenkins, Artifactory and Spring Cloud Contract

Consumer Driven Contract (CDC) testing is one of the methods 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 Jenkins server for running contract tests within our Continuous Integration (CI) process.

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

contracts-3 (1)

The source code of sample applications is available on GitHub in the repository sample-spring-cloud-contract-ci (https://github.com/piomin/sample-spring-cloud-contract-ci.git). I placed all the sample microservices in the single Git repository only for our demo simplification. We will still treat them as separate 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 stubs suffix, that contains all defined contracts and JSON mappings. Such a JAR file can be built on Jenkins and then published on Artifactory. Contract consumers also use the same Artifactory server, so they can use the latest version of stubs file. Because every application expects different response from person-service, we have to define three different contracts between person-service and a target consumer.

contracts-1

Let’s analyze the sample scenario. Assuming we have performed some changes in the API exposed by person-service and we have modified contracts on the producer side, we would like to publish them on a shared server. First, we need to verify contracts against producers (1), and in case of success publish an 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 JAR file with stubs (3). Then, the newest version contract is verifying against consumer (4). If contract testing fails, pipeline is able to notify the responsible team about this failure.

contracts-2

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 for running 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 such 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 a first launch. It also automatically add one remote repository: JCenter Bintray (https://bintray.com/bintray/jcenter), which is enough for our build. Jenkins also comes with a default set of plugins, which you can install just after first launch (Install suggested plugins). For this demo, you will also have to install plugin for integration with Artifactory (https://wiki.jenkins.io/display/JENKINS/Artifactory+Plugin). 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 Person object are used by consumers. As you see, some of the fields are shared between consumers, while some others are required only by single consuming applications.

contracts-4

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 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 Spring Cloud Contract tests on the producer side

Ok, 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 different response for the same request, and 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 directories bank-consumer, contact-consumer and letter-consumer.

contracts-5

All the contracts will use a single base test class. To achieve it 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 base class for our contract tests. We will mock the repository bean with the answer matching to the rules created inside 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());
   }
   
}

Spring Cloud Contract Maven plugin visible above is responsible for generating stubs from contract definitions. It is executed during Maven build after running mvn clean install command. The build is performed on Jenkins CI. Jenkins pipeline is responsible for updating remote Git repositories, building binaries from source code, running automated tests and finally publishing JAR files containing stubs on a remote artifact repository – Artifactory. Here’s 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 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 Spring Cloud Contract tests on the consumer side

To enable Spring Cloud Contract on the consumer side we need to include 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>

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

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

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

5. Setup of Continuous Integration process

Ok, 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 source code. Now, our goal is to launch pipelines defined for three consumer applications, each time when new stubs would be published to the Artifactory server by the producer pipeline.
The best solution for that would be to trigger a Jenkins build when you deploy an artifact. To achieve it we use Jenkins plugin called URLTrigger, that can be configured to watch for changes on a certain URL, in that case REST API endpoint exposed by Artifactory for selected repository path.
After installing URLTrigger plugin 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, that is accessed via the following URI: http://192.168.99.100:8081/artifactory/api/storage/[PATH_TO_FOLDER_OR_REPO]/. The file maven-metadata.xml will change every time you deploy a new version of application to Artifactory. We can monitor the change of 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.

contracts-6

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

contracts-7

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

contracts-8

The JAR file with contracts is published under classifier stubs.

contracts-9

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

[code]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 changes in contract

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

contracts-12

This change requires a change in contract definition created on the producer side. If you modify the field name there person-service will build successfully and a new version of 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. Microservices letter-service and contact-service do not use field accountNo, so their pipelines will not fail. Only bank-service pipeline report error in contract as shown on the picture below.

contracts-10

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

contracts-11

2 COMMENTS

comments user
press0

Great article. Thanks for doing the heavy-lifting and showing the details.
Possible correction in the first sentence. Is the first C in CDC “Contract” or “Consumer”.

    comments user
    Piotr Mińkowski

    Thanks. Fixed:). I missed that sentence

Leave a Reply