Microservices, Test your code

Testing microservices

Now the fun part. Assuming that we have built a running microservice environment, how can we maintain it? How do we introduce changes without breaking it? How do we test it? We know we can do end to end tests, but these are slow and we need faster feedback. How can we test the microservice in isolation?

One solution that gains traction is Consumer Driven Contract Testing (CDC-T). Let’s examine the words that build this structure.

What is a testing?

 We need to make sure that our service works as expected, and more importantly can be deployed without breaking the dependent services. CDC-T is a form of unit testing if you consider your service a unit. But since the service is composed of multiple modules it is integration testing, as you test that the modules that compose the service integrates with each other. Since a unit means the smallest component possible, that means CDC-T is a form of integration testing. We all know testing something means using test doubles. When using test doubles we need to make sure they are not out of sync with the actual service/client that consumes them. This leads us to the next term.

What is a contract?

A contract is a set of rules that must be followed. For example an interface is a contract. It exposes a set of operation signatures that an implementation must follow. A contract can contains also the type of communications. From the service point of view, it refers to its rest api (if we are talking about rest services). This api is being used by one or more consumers. This is the contract that the service as a provider is offering.

Why consumer driven?

Because the clients are the consumers. They are deciding what its important, and their requirements must be met. The consumers must decide what the contract should contain, and the service must follow that without breaking other consumers. A service provider can be consumed by one or more clients. That means a service can be bound to multiple consumer contracts.

Putting these together, CDC-T is a way of making sure that the contracts specified by the clients are not broken by writing tests using contract stubs. By stubbing these contracts we can test the client without the presence of a service, therefore achieving quick feedback. One huge advantage of this is that the services are driven by the business needs. In a way CDC-T is a form of TDD applied on a different level.

Spring Contract

Spring contract implements this pattern. On the consumer side Spring Cloud Contract generates stubs, which you can use during client-side testing. You get a running WireMock instance/Messaging route that simulates the service. You would like to feed that instance with a proper stub definition.

On the service side you need to be sure that the stub it actually resembles your concrete implementation. You cannot have a situation where your stub acts in one way and your application behaves in a different way, especially in production. To ensure that your application behaves the way you define in your stub, tests are generated from the stub you provide.

Ok, let’s see put these in practice. On the producer side we have the contracts that the consumers require (this is a hardcoded contract)

package contracts

org.springframework.cloud.contract.spec.Contract.make {
    request {
        method 'GET'
        url '/items/A'
        headers {
            header('Content-Type': consumer(regex('application/json')))
        }
    }
    response {
        status 302
        body(
                    code    : 'A',
                    reservePrice   : value(producer(regex('[0-9]+')))
        )
        headers {
            header('Content-Type': value(
                    producer('application/json;charset=UTF-8'),
                    consumer('application/json;charset=UTF-8'))
            )
        }
    }
}

and the Spring Cloud Contract Verifier maven plugin

 <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.so.cdct.ItemBaseTest</baseClassForTests>
       <basePackageForTests>com.so.cdct</basePackageForTests>
    </configuration>
 </plugin>  

which

  • generate and run tests

  • produce and install stubs

The baseClassForTests property must contain the initialization of the RestAssuredMockMvc. The contract verifier test will extend this class. It will look like:

public class ContractVerifierTest extends ItemBaseTest {

   @Test
   public void validate_shouldReturnItem() throws Exception {
      // given:
         MockMvcRequestSpecification request = given()
               .header("Content-Type", "application/json");

      // when:
         ResponseOptions response = given().spec(request)
               .get("/items/A");

      // then:
         assertThat(response.statusCode()).isEqualTo(302);
         assertThat(response.header("Content-Type")).isEqualTo("application/json;charset=UTF-8");
      // and:
         DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
         assertThatJson(parsedJson).field("code").isEqualTo("A");
         assertThatJson(parsedJson).field("reservePrice").matches("[0-9]+");
   }
}

What the above contract is basically saying is that the consumer expects a service that is able to return the item with the code A. And for this request the server will return some fake data.

On the consumer side, the maven dependency

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

will give us the ability to run the stubs. The @AutoConfigureStubRunner will inject the item service as a stub. It will look in the local maven repo (workOffline = true) for the artifact

 <dependency>
    <groupId>com.so</groupId>
    <artifactId>item-service</artifactId>
    <version>1.0-SNAPSHOT</version>
 </dependency>

The + sign means that it will resolve to the latest version.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
@AutoConfigureStubRunner(ids = {"com.so:item-service:+:stubs:8082"}, workOffline = true)
public class BidServiceTest {

    @Autowired
    private RestTemplate restTemplate;

    @Test
    public void testBidShouldBeAdded() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        HttpEntity entity = new HttpEntity<>("{\"itemCode\":\"A\",\"amount\":\"500\"}", headers);
        ResponseEntity addBid = restTemplate.postForEntity("http://localhost:8081/bids", entity, String.class);
        assertTrue(addBid.getStatusCodeValue() == 200);
        assertTrue(addBid.getBody().equals("Bid confirmed"));
    }
}

When running this test the consumer service will start and along with it the stubbed provider service. The test will pass without the presence of real provider service, as long as WireMock request mappings contains the request which is being made on the provider service. Those mappings are determined from the contracts.

At this point I recommend to use the exchange method of the RestTemplate since it allows you to control headers and avoid WireMock stubbed request not matching the real one.

HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity entity = new HttpEntity(headers);
return restTemplate.exchange("http://localhost:8082/items/" + code, HttpMethod.GET, entity, ItemResponse.class);

That’s it. The producer creates and publishes stubs which will be consumed by the clients using a maven repository as a central repo.

SOURCE CODE

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google+ photo

You are commenting using your Google+ account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

w

Connecting to %s