Getting microservice specification right the first time

Hassle-free development through TDD and ATDD practices

Team Falcon
5 min

What’s a Rich Text element? What’s a Rich Text element? What’s a Rich Text element?

What’s a Rich Text element? What’s a Rich Text element? What’s a Rich Text element?

What’s a Rich Text element? What’s a Rich Text element? What’s a Rich Text element?

The rich text element allows you to create and format headings, paragraphs, blockquotes, images, and video all in one place instead of having to add and format them individually. Just double-click and easily create content.

  • The rich text element allows you to create and format headings, paragraphs, blockquotes, images, and video all in one place instead of having to add and format them individually. Just double-click and easily create content.
  • The rich text element allows you to
  • The rich text element allows you to create and format headings, paragraphs, blockquotes, images, and video all in one place instead of having to add and format them individually. Just double-click and easily create content.

Static and dynamic content editing

A rich text element can be used with static or dynamic content. For static content, just drop it into any page and begin editing. For dynamic content, add a rich text field to any collection and then connect a rich text element to that field in the settings panel. Voila!

An unhealthy work-life balance (source)

How to customize formatting for each rich text

Headings, paragraphs, blockquotes, figures, images, and figure captions can all be styled after a class is added to the rich text element using the "When inside of" nested selector system.

Introduction

With a growing focus on adopting microservices, developers often find it frustrating when someone modifies an API in a microservice. It is incredibly difficult to fully understand the impact of that change, which makes it difficult to throw blame around. It’s also hard to search through all microservices calling that API.

At Falcon, we have spent months developing a microservice architecture that improves production reliability, which ultimately benefits the end user. We have adopted an approach that notifies the developers of microservices that a change has been introduced, forcing developers to update their code before pushing it to their source code management tool.

Keep scrolling if you

  • Are a professional software engineer working on back-end systems
  • Are fed up with managing ever-changing request/response cycles and URLs across different microservices
  • Are looking for test-driven development (TDD) at the software architecture level
  • Understand the basic concept of microservices. No? For a proper introduction please read Microservices by Martin Fowler.

Problem 1: modifying existing APIs

With all the buzz about microservices, there is a real challenge when someone wants to:

  1. Alter the request/response structure or URL of an endpoint
  2. Modify their code so that the endpoint gives a different response even if the structure remains the same

This situation worsens when the number of microservices increases in a system landscape. If different microservices call this endpoint, then a developer has to change the structure in all of them. In order to do that, we need to:

  • Identify the calling services that are impacted by this change
  • Manually check out all repositories and update them

Looking at Netflix, which has over 600 microservices, you’ll probably end up like this guy:

An unhealthy work-life balance (source)

Problem 2: testing a microservice that calls other services

For testing a microservice, we have two options:

  • Deploy all microservices and perform end-to-end tests
  • Mock other microservices in unit/integration tests

Both have their advantages but also a lot of disadvantages:

End goal: having a contract between microservices

This means:

  • Getting a build-time error when any changes are made to existing APIs
  • Ensuring that the HTTP/messaging stubs (used when developing the client) actually correspond to server-side implementation
  • Auto-generating boilerplate test cases on the server side based on the contract (a nice-to-have feature)

Solution

It’s not one silver bullet, but a combination of two things:

  1. Integration tests on the consumer side: using MockMvc, RestAssured or any other library
  2. Spring cloud contract: an umbrella project in Spring that helps us implement a consumer-driven contracts (CDCs) approach

For the first point, many blogs and documents describe how to write test cases that call REST endpoints and then push the expected response. These libraries have existed for several years, and are well-established.

We’re focusing on Spring Cloud Contracts and how to write CDCs. If you don’t know what CDCs are, check out Consumer-Driven Contracts: A Service Evolution Pattern by Martin Fowler.

Why CDCs?

Falcon opts to use CDCs because they promote Acceptance Test-Driven Development (ATDD). This reduces conflicts that occur at later stages when we couple microservices. Through this approach, everyone is well versed in even the minute details of an API.

The big picture

  • A new requirement comes up to develop a functionality
  • The team analyzes the requirement to identify the microservices involved
  • They generate contracts on the producer side in simple Groovy language
  • This generates test cases on the producer side automatically and creates stubs for consumers
  • On the producer side, the developer writes code to correct all failed test cases
  • On the consumer side, the developer writes integration test cases and then proceeds to correct all failed test cases. All calls to other microservices are mocked; their dummy responses are made available by a Spring-cloud-contract-verifier. The test data is imported from the stub jar created on the producer side.

Analyzing the producer and consumer sides

To explain the approach, let’s look at a simple example — a user calls a GET sentences API for n sentences. The consumer and producer are two microservices. The consumer builds a request with random words and sends a POST request to the producer. The producer then builds a sentence and returns it to consumer. The consumer gathers all sentences and returns an HTML page with all sentences.

Sequence diagram for demo application

The producer side

We write simple groovy script that depicts the actual request and the associated response. Running mvn install goal on this project will result in the automatic generation of a test case, which will call the endpoint with the dummy request as stated in the contract. This will also bring an API response against the dummy response stated in the contract, and will generate stubs in a jar that you can use on the consumer side to mock API calls to this endpoint (we’ll get to that part later).

Step 1

We need to add a spring-cloud-contract-verifier dependency in pom.xml, as well as a spring-cloud-contract plugin.


<dependencies>
  <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-contract-verifier</artifactId>
     <scope>test</scope>
  </dependency>
</dependencies>
<build>
  <plugins>
     <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <extensions>true</extensions>
        <configuration>
<baseClassForTests>com.akul.contract.producer.BaseClass
</baseClassForTests>
        </configuration>
     </plugin>
  </plugins>
</build>

Step 2

Earlier in pom.xml, we mentioned “BaseClass” while configuring the spring-cloud-contract plugin. The BaseClass is a simple SpringBootTest class that will be inherited by the generated test class and will provide the Spring context.

We also have an option to provide a base package for multiple base classes. This might be useful in a more complex project, but in this example, we have used one BaseClass for simplicity.


@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.MOCK)
@DirtiesContext
public class BaseClass {
  
  @Before
  public void setup() {
    StandaloneMockMvcBuilder standaloneMockMvcBuilder = 
        MockMvcBuilders.standaloneSetup();
    
    RestAssuredMockMvc
        .standaloneSetup(standaloneMockMvcBuilder);
  }
}

Step 3

We’re going to write the contract in Groovy.

The contract should be placed in the package /src/test/resources/contracts/


import org.springframework.cloud.contract.spec.Contract
import org.springframework.http.MediaType
Contract.make {
description "Should return sentence from words"
request {
       method POST()
       url("/words")
       headers {
           contentType(MediaType.APPLICATION_JSON_VALUE)
       }
       body([
               subject : "I",
               verb: "saw",
               article: "an",
               adjective: "incredible",
               noun: "performance"
       ])
       stubMatchers {
           jsonPath('$.subject', byRegex(onlyAlphaUnicode()))
           jsonPath('$.verb', byRegex(onlyAlphaUnicode()))
           jsonPath('$.article', byRegex(onlyAlphaUnicode()))
           jsonPath('$.adjective', byRegex(onlyAlphaUnicode()))
           jsonPath('$.noun', byRegex(onlyAlphaUnicode()))
       }
   }
response{
       status(200)
       headers {
           contentType(MediaType.APPLICATION_JSON_VALUE)
       }
       body([
               id : "a62fe6d7-f898-44b7-8aba-6eaddece31be",
               sentence : "I saw an incredible performance"
       ])
       testMatchers {
           jsonPath('$.id', byRegex(uuid()))
       }
   }
}

It’s ok if you don’t know the syntax of groovy. If you’re using an IDE such as Intellij, you’ll get automatic suggestions.

Most of the contract is easy to comprehend. We are sending a POST request at /words URL with a JSON body containing: a subject, verb, article, adjective and noun. We expect a response with HTTP status 200 (ok), with the JSON body containing the UUID and sentence.

stubMatchers and testMatchers are optional parts to the above contract that help us in the following way:

stubMatchers (for the consumer side) define the dynamic values that should end up in a stub. A particular pattern of values in the request returns a response stated in the contract.

testMatchers (for the producer side) define regex for dynamic values in the response. In the above case, a random UUID is generated for each response, and we want our test case to pass for any UUID value; testMatchers provide this flexibility.

At this point we can run an mvn clean install which will automatically generate the test case. However, we’ll get a build failure, because we have not created a REST controller to perform the operation.

Also, we can share the generated stub with the developer working on the consumer side. This will enable the developer to start their work following a TDD approach. The generated stub will be installed in an .m2 folder. The generated stubs will be present in the output folder under /stubs/mapping/

Step 4

Now let’s finish our work by writing a REST controller


@RestController
public class SentenceController {
@PostMapping(path = "/words", 
   consumes = MediaType.APPLICATION_JSON_VALUE, 
   produces = MediaType.APPLICATION_JSON_VALUE)
  public SentenceResponse createWord(@RequestBody SentenceParams sentenceParams) {
SentenceResponse response = new SentenceResponse();
response.setId(UUID.randomUUID().toString());
    response.setSentence(sentenceParams.getSubject() + " "
           + sentenceParams.getVerb() + " "
           + sentenceParams.getArticle() + " "
           + sentenceParams.getAdjective() + " "
           + sentenceParams.getNoun());
return response;
  }
}

and Autowire the same in our BaseClass. This is required to set up RestAssuredMockMvc in the BaseClass that we need for the generated test case.


@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.MOCK)
@DirtiesContext
public class BaseClass {
@Autowired
    private SentenceController sentenceController;
  
    @Before
    public void setup() {
        StandaloneMockMvcBuilder standaloneMockMvcBuilder = MockMvcBuilders.standaloneSetup(sentenceController);
        RestAssuredMockMvc.standaloneSetup(standaloneMockMvcBuilder);
    }
}

Now, if we build our application by mvn clean install, we’ll get a successful build and we can rest assured that the API works as it is supposed to. The generated test case will be present at:

/target/generated-test-sources/contracts/ContractVerifierTest


public class ContractVerifierTest extends BaseClass {
@Test
  public void validate_shouldReturnSentenceFromWords() throws Exception {
     // given:
        MockMvcRequestSpecification request = given()
              .header("Content-Type", "application/json")
              .body("{\"subject\":\"I\",\"verb\":\"saw\",\"article\":\"an\",\"adjective\":\"incredible\",\"noun\":\"performance\"}");
// when:
        ResponseOptions response = given().spec(request)
              .post("/words");
// 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("['sentence']").isEqualTo("I saw an incredible performance");
     // and:
        assertThat(parsedJson.read("$.id", String.class)).matches("[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}");
  }
}

The consumer side

On the consumer side, we need to develop an API that calls the producer-side API. First, we need to write an integration test case for the consumer-side API. The stub generated on the producer side will be used to deploy a mock server that will respond to the request sent from the consumer API to the producer API.

Any change on the producer side will require a change in the contract. And any change in the contract will produce a stub that responds differently. By writing an integration test case, we are making sure that we get a build time failure. If any changes are made on the producer side without informing the developer who is working on the consumer side, we can immediately address the concern and stop the code release to QA or to the production environment.

Step 1

We need to add pom dependencies to provision a mock server that will run the stubs.


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

Step 2

Because we are following TDD, we need to write an integration test case that calls the consumer-side API. In this test class, we are going to configure the stub runner to mock API calls to the producer through simple annotations.


@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
@AutoConfigureJsonTesters
@AutoConfigureStubRunner(workOffline = true, ids = "com.akul.contract:producer:+:stubs:8020")
public class ConsumerApplicationTests {
  @Autowired
  private MockMvc mockMvc;
  @Test
  public void validate_getSentences_shouldReturnSentences() throws Exception {
    // GIVEN
    String expectedContent = "I saw an incredible performance

" + "I saw an incredible performance

"; // WHEN mockMvc.perform(MockMvcRequestBuilders .get("/sentences?number=2") .contentType(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) .andExpect(content().string(expectedContent)); } }

The test case is pretty basic. The important things to note are the annotations above the class declaration. Note the IDs property inside the @AutoConfigureStubRunner annotation. The format is:

<groupId>:<artifactId>:+:stubs:<server port>

These properties are the actual properties of producer side application.

com.akul.contract — is the group ID.

producer — is the artifact ID.

8020 — is the port on which the stub will run.

That’s it! We have successfully installed the stub, configured a mock server to run this stub and written a test case for our consumer-side API. In short, we have written a test to perform end-to-end testing of our feature API: /sentence that returns random sentences based on the query parameter number in the URL.

In order to finish our work, we need to write a controller that will perform an HTTP call to the producer API. In my example, I have used RestTemplate to call the producer-side API.


@GetMapping(path = "/sentences")
public String getSentences(@RequestParam("number") Integer number) {
StringBuilder sentences = new StringBuilder(number);
for(int index=0; index<number; index++) {
       sentences.append(getSentence());
       sentences.append("<br><br>");
   }
return sentences.toString();
}
private String getSentence() {
   HttpHeaders httpHeaders = new HttpHeaders();
   httpHeaders.add("Content-Type", "application/json");
HttpEntity<SentenceParams> request = new HttpEntity<>(getSentenceRequest(), httpHeaders);
ResponseEntity<SentenceResponse> responseEntity = restTemplate.exchange(
           "http://localhost:8020/words",
           HttpMethod.POST,
           request, SentenceResponse.class);
return (responseEntity.getBody().getSentence());
}

The full implementation of this tutorial can be found on my GitHub. The official Spring cloud contract reference document is another valuable resource.

Summary

With this approach:

  • Every member of a team understands even minute details of the API stated in the contract
  • The consumer application is notified the instant there is a change in producer API
  • We are able to perform end-to-end testing of all APIs on the consumer side, irrespective of whether the producer API is developed or not
  • We get auto-generated integration test cases on the producer side

Soar into the future with us