lastminute.com logo

Technology

Impacts of contract tests in our microservice architecture

ivan_delloro
ivan delloro

We learned how to implement contract test for any kind of messages exchanged in a microservices' architecture, now let's see the impacts and why it sped up our development process


Ivan is a product engineer with 8+ years of experience, 6 of which in lastminute.com group. He’s actually working in the flight supply domain.

Contract test instead of integration tests

In a previous article published by my colleagues about how to use Pact and MockK, they well explained how to implement contract testing. Still, I’d like to go in deep on why in some cases, contract tests are the best solution to verify the proper behavior when, in a microservice’s architecture, two or more components need to exchange a message. Why don’t we use integration tests also to verify the correct integration between two microservices? We can run these tests in our QA environment and be sure that this integration works.
For some reason, in lastminute.com group, we used to call Integration Test also the tests which made actual calls between two microservices, and interact with databases and all the other external components, even if they were supposed to be more likely system tests. So when in this article I will mention IT tests, I will refer to those.
In the past, we had tons of IT tests, but unfortunately, they were weak and failure-prone for reasons different from what we aimed to verify.
For example, they used to fail because the called service was unreachable in the QA environment at that moment but was perfectly working in the production environment. Another reason for which such tests were failing was because they were also integrating with our QA database, and to give an answer during the test phase, we were reading some rows, but someone changed those rows manually for testing reasons. The result was always the same: these tests were ignored, and none of these tests covered the integration between our beloved microservices. Here we can see a simplified version of such a kind of test:

  public class CallServiceTestIT{

    @Test
    @Throws(InterruptedException.class)
    public void testCall(){
      String id = UUID.randomUUID().toString();
      Client client = new Client();

      ServiceResponse response = client.callWithId(id);

      assertThat(response.getStatusCode(),is(200));
      assertThat(response.getId(),is(id));
      assertThat(response.getLanguage(),is("en"));
      assertThat(response.getPrice(),is(205.57d));

    }

  }

In the example above, we are creating the client used to make the call, the unique identifier, the id for what we aim to retrieve, and the assertions for the result. Indeed, this test is easy to write and read, but unfortunately, we cannot say it’s easy to maintain: since we are dealing with a real rest call to another service, many things can go wrong. My colleagues have provided an obvious and complete example of a contract test in the article linked above: I suggest to have a look at it, because it explains a complete example contract test with Pact.
Using contract tests, we verify that when a microservice sends a message of any kind, the answer will be the one we expect, without needing an actual real response from the target microservice. Also, one of the reasons we write tests is to receive feedback: the shorter the feedback cycle, the faster the development process. There are two main ways to implement contract tests: consumer-driven and producer-driven. In this article, I will refer to consumer-driven ones, but the same considerations are still valid for any kind of contract tests.

Impact on the feedback cycle and development process

As mentioned before, when we used to implement integration tests to verify the message exchange between two microservices, they failed for multiple reasons. This weakness leads us to ignore those tests to avoid blocking the development process. The result was that they were left ignored for months, and when something changed on the other side, both the CI pipelines were green: usually, we realized there was an error in the contract when something in production failed. We need time to investigate the reasons for the failure, involve other teams and ask for a rollback, or fix the issue as fast as possible.

For example, as we can see in the message below, our colleague Roberto asked for help to fix a test failure in QA.

message
slack message

Other times, the root cause of the test failure was located in some data stored on a database that someone or some other application changed. In these cases, debugging was even more expensive in terms of effort and time.
Consequently, the time to market for what we need to release expanded. Since none of us liked to take action to fix tests for almost every release, these tests were marked as ignored, with the result that nothing covered the message exchange. Since in a microservice architecture the communication part is an important element of every microservice, we need these steps covered by tests, because, instead of having communication inside the monolith, in a distributed system we have remote communication who works through interconnection of systems, who must talk through the same language, or in our context, must have the same contract.

contract
microservices contract

There needs to be more than testing the contract to verify the proper communication of the microservices. Still, since this kind of test is localized to the contract, it will fail if it has not been respected or has been changed in one of the two parts (caller or called service), and the debug part requires only the time to check the changes on the contract itself.

Impact on the feedback cycle and the lead time

Let’s now consider the execution of a common Spring application that runs IT tests. When they are executed, they need to start the entire application and then run the tests.
To do so, it needs to create all the configuration objects, connect to databases, message brokers, and so on: considering a typical application we have in lastminute.com group, the startup time itself can be around a minute, and, it still has to run the tests. Also, to execute an integration test with a simple call, we need to start our service, call the target and wait for the response. When each one is connected to many others, the testing execution time will increase rather quickly, and so will our feedback cycle.

Instead, to execute the Pact tests, we don’t need the application to be up and run (locally or in the CI tool), but we need to contact the Pact broker: the execution time for a few tests is less than a second! Consequently, the test execution time is shorter. We can react faster and reduce our development time and, of course, the cycle time.

Contract tests are not enough

With contract tests, we verified that our microservices “speak the same language”, but it is not enough to prove the whole communication part. All the communication steps consist of many components, like, in the Rest sync communication, the call logic with its faults (since we are using infrastructure and network), and the controller and its logic. On both sides, we need to verify the behaviors. Still, since we excluded any possible fault due to the contract, we can (we must!) test them in isolation, with a low execution time and, not less significantly, easier to understand and debug.


Read next

SwiftUI and the Text concatenations super powers

SwiftUI and the Text concatenations super powers

fabrizio_duroni
fabrizio duroni
marco_de_lucchi
marco de lucchi

Do you need a way to compose beautiful text with images and custom font like you are used with Attributed String. The Text component has everything we need to create some sort of 'attributed text' directly in SwiftUI. Let's go!!! [...]

A Monorepo Experiment: reuniting a JVM-based codebase

A Monorepo Experiment: reuniting a JVM-based codebase

luigi_noto
luigi noto

Continuing the Monorepo exploration series, we’ll see in action a real-life example of a monorepo for JVM-based languages, implemented with Maven, that runs in continuous integration. The experiment of reuniting a codebase of ~700K lines of code from many projects and shared libraries, into a single repository. [...]