Continuing the study of the topic of microservices , we decided to offer you a translation of an article on testing MOA (microservice-oriented applications). Recently we have already turned to the topic of testing , but in the case of microservices, regular unit testing is not enough, you also need to consider aspects related to CI / CD and other things that will be discussed in this article.
Microservices are a new style of software architecture used to create distributed systems and are increasingly being implemented in companies operating on the web, including the largest of them. For example, Netflix and Amazon have adopted microservice architecture to significantly accelerate the release of software products. At the same time, older monolithic systems cannot scale at a speed that would meet modern challenges.
But the benefits that a microservice architecture can bring is only as good as the best practices for supporting it can be tested. When designing tests to validate microservices systems, where the release rate can be described as lightning fast, innovative thinking is required, both at the micro and macro levels.
Next, we will explore the three types of microservice-oriented applications, discuss some of the challenges you will have to deal with in order to test them, and also talk about how tests are written to fully appreciate the merits of microservices.
Briefly about microservices
Before we get into some of the intricacies of designing tests for microservices, let's take a look at what microservices are. A microservice is a detailed software component that has been given a clear semantic definition; however, the microservice carries its own data set and does not depend on other data structures and data sources. Each microservice has its own deployment cycle, so after changes have been made to the microservice, you can release it without interrupting the work of any other microservices in the scope of the application with which this microservice is running.
Advantages of microservices over monolithic applications
Let's also take a look at what a microservice is not. The following figure shows an example of a typical monolithic application.
Figure: 1: Monolithic application architectures typically have strong component cohesion
The Customers, Products, Orders, and (Comments) components can be thought of as a set of classes written in an object-oriented language such as Java or C #, where customers is an array of objects corresponding to customers, products - an array of objects corresponding to products, etc. The customer object can take advantage of both the customer object and the product object. Or, due to the fact that all the components of a monolithic application access the same database, the order object may well access the database directly and get all the information about the product and order that is needed to complete the tasks. Direct access to the database is not only possible - and how much it contradicts the spirit of object-oriented programming based on an object-relational mapping ORM - but it is also actively used, especially in those areas where there is a high demand for ready-made features by the agreed date, no matter at what cost.
The result is a tightly coupled, sometimes fragile system, where releasing a new version of an application requires significant coordination among all development teams involved in the development of that system. Because of the strong coupling, the entire release cycle cannot go faster than the slowest work of renegotiating a dependency. In other words, if it is time to update the application and add a new feature by customers and a new feature by products to it, then the release will not take place until both of these features are ready. If it takes one day to make a product update and three weeks to make a customer change, then the release will take place no earlier than three weeks. Moreover, which further exacerbates the situation, during the release, it may be necessary not only to coordinate the new code concerning customers and products,but also make changes to the database schema, and this will also have to be managed during the release process.
Releasing changes to a database is a very delicate undertaking. There is always the risk of unintended side effects when changing the structure of the database. Remember that if any component can access the database, then it will access it, and even perform such operations on the data that are not within the scope of this component. Such "out-of-area manipulation" can lead to failures of other components, and such failures can go unnoticed until a disaster occurs. Sometimes a potential danger can be found in the code right up to the release of this code into production. Unfortunately, this happens all the time.
Some companies are willing to put up with the slow release cycles that are typical for working with monolithic applications. But, if we are talking about a company that provides support for hundreds of thousands of users, and at the same time must maintain thousands of components in working form, then the pace "no faster than the release of the most late component" is unacceptable.
Microservice-oriented applications
In a microservice-oriented application (MOA), each functional component is decomposed into the smallest possible units that have a pronounced functionality (within reasonable limits). In this case, each such functional component is organized as a microservice. Each company has its own legacy code baggage, as well as a corporate culture that should be adhered to, so it is impossible to write "like notes" how exactly the decomposition should be carried out.
When it comes to decomposing a monolithic application in MOA, the key to remember is that the best is the enemy of the good. Make sure that each microservice is structured according to its semantic definition, that the microservice has its own data, as well as its own release cycle. The depth of implementation depends on what the company can afford, based on the time available, employee experience, and available resources. Some microservices may have just one function, others may have many functions.
There are three types of MOA : synchronous , asynchronous, and hybrid .
Synchronous microservice-oriented applications
Figure 2 shows a synchronous MOA. Interservice communication is a request-response principle typical of HTTP interactions on the web.
Figure 2: Microservice-Oriented Application Based on Synchronous Interservice Communication
Each microservice is segmented behind the HTTP server, which allows access to the logic of this microservice. A specific microservice only knows its own area of responsibility; he may also know the interface for interacting with another microservice, but he cannot see the internal logic of another service. In addition, the microservice only knows about its own data. The data stores of other microservices are not known to him and are not available to him. It is impossible to "hijack" the data of another microservice by directly accessing the data warehouse for it. The only way to get data from the microservice and transfer new data to it is to interact with it through the public interface.
Among the advantages of synchronous MOAs is their independence. For example, the Customers microservice shown above can update at any time it suits it. There are no external dependencies that need to be adjusted to this. As long as the microservice does not change its public interface, as well as the structure of the data that it intends to consume from the request and return as a response, the risk of breaking the entire MOA architecture is minimal.
By its very nature, the MOA architecture is such that it maintains its own structural integrity. Since the microservice itself carries its own data, its work does not affect the data stores of other services. Since a microservice is represented as a set of HTTP URLs and related data structures, built on the basis of a request-response principle, the boundaries of the microservice's interface are clearly defined.
Synchronous MOAs are becoming more and more popular. Many synchronous MOA interfaces are based on the REST paradigm, a style that has been around since 2000of the year. But, for all its popularity, MOA has a drawback: low speed. Consumers of a synchronous microservice will never run faster than this microservice can handle requests and responses. This can be a major hindrance, especially when dealing with microservices, whose processes take a long time to execute. An example is a complex analytical service that consumes and processes terabytes of data. Few customers will agree to sit and wait for a few minutes in a row for the service to shut down. It would be more convenient to tell the microservice what work needs to be done and then get notified when the results are ready.
In situations like this, it is more convenient to take an asynchronous approach to designing microservices.
Asynchronous microservice-oriented applications
Figure 3 below shows an asynchronous implementation of MOA, where interservice communication is provided by exchanging messages between the parties involved in the work. Typically, this interaction is called “fire and forget”.
Figure 3: Asynchronous MOA architecture is based on messaging
In the asynchronous MOA architecture, messages can be generated randomly or in response to a given event. For example, when (as shown in the picture above) an order is created in the Orders microservice, this service can publish an event
orders_created
and place it in a companion message queue., which is listening to another microservice, billing (not shown in the figure) and accepting incoming messages. The billing microservice picks up the order information message and processes it in a way that is relevant from a billing perspective.
The advantage of an asynchronous approach to designing microservice-oriented applications is that there are no bottlenecks in such a system and, therefore, it is extremely efficient. The disadvantage is that such a system is very difficult to create and then manage.
For large scale asynchronous systems like Uber, it is normal to process thousands of messages per second. Debugging such a system is difficult because its workflows do not have clear trajectories. It is not about doing one action first and then doing another; the logic is triggered depending on the message received, that is, anything can happen at any time.
This should be kept in mind when developing testing strategies. For example, an asynchronous system is impractical, the performance of which depends on the request and response time, since there will never be a one-to-one correspondence "one request-one response".
Hybrid microservice-oriented applications
The balanced approach to implementing a microservice-oriented application is hybrid. Services are simultaneously synchronous, that is, direct request-response communication is supported between them, and at the same time asynchronous; that is, some of the messages are generated in conjunction with or as a result of synchronous messaging.
Figure 4 illustrates the communication patterns used in a hybrid approach to microservice-oriented applications. The Customers microservice is represented both as a URL associated with an HTTP server and as a queue in a message broker.
Figure 4: A hybrid approach to designing microservice-oriented applications that uses both synchronous and asynchronous communication options between services and consumers.
You can add a client to the microservice using standard HTTP request-response communication. The request is received and processed, then a response is generated. However, before a response is generated, a message with information about the new client is published to the companion message queue so that other interested services can consume it. The information sent to the message queue can be the same as generated in the HTTP response, or slightly different, at the discretion of the microservice. It all depends on how the microservice is designed and the QoS agreements that the microservice must enforce.
GraphQLIs an API technology that supports both synchronous and asynchronous communication between a consumer and a service. Learn more about this technique and GraphQL subscriptions here .
The most important feature of the hybrid approach is that it combines the merits of the other two approaches. But there are accompanying disadvantages, in particular, potential bottlenecks that reduce performance, and the additional complexity associated with the need to support two fundamentally different types of interfaces used by the microservice "in" and "out".
Difficulties arise when designing tests for microservices
When it comes to testing such applications, the first thing to remember is that it is very rare that one microservice is shared by multiple MOAs. Typically, a microservice is one of many in a specific domain within an application. The virtue of a microservice-oriented approach to application architecture design is faster code transfer to and from production. Netflix, for example, has over 4,000 deployments a day .) This release rate is simply not possible in a traditional monolithic application environment.
It should also be remembered that MOA testing should take place at both the micro and macro levels.
Micro-level testing
At the micro level, each service must be thoroughly tested within its area of responsibility. According to some interpretations, a function is considered a microservice boundary.
Go beyond traditional unit testing
It is advisable to pay special attention to the function within the microservice, given the growing popularity of the serverless paradigm, according to which the microservice should only be presented as a single function. But many practicing testers tend to limit themselves to unit tests when working with microservices. While a unit test normally covers a single function that works in a very limited test environment, a microservice is designed to serve an audience across the web. Therefore, testing conditions must be extreme.
For example, as part of a good micro-level test, you can run one hundred thousand instances of a microservice at the same time and see how they behave on that scale. It's not enough to test just one function at a time by applying a single unit test to it. It is necessary to run such tests on thousands of instances of the function at the same time, taking into account the indicators of the hosting environment in which you will have to work.
Test your unit of deployment
Along with testing the functionality of the microservice, you also need to take care of testing the deployment unit within which the microservice is released. Typically, microservices are deployed as containers as part of an orchestration technology such as Kubernetes or Docker Swarm . An important aspect of container orchestration is ensuring the long-term resilience of microservices.
It is believed that microservices can fail for a variety of reasons, both due to host failure and due to errors in the microservice itself. It is quite normal that during the simultaneous operation of thousands of containers, some kind of current malfunction occurs. The importance of testing is that the correct exit of the microservice from the game is no less important than its correct functioning. Micro-level tests ensure that the microservice will neatly come to life and neatly die, at any scale of the system.
Make sure that absolutely everything that happens to you is logged
The tests must also ensure that all significant events occurring in the microservice are properly logged - and, just as important, these log entries can be understood. In the world of microservices, logging is very important, especially for asynchronous MOA where there is no sequential execution of behaviors. Often, only the log data will help you comprehend what is happening in the application.
Macro testing
Whether the MOA is synchronous, asynchronous, or hybrid, it is shown rigorous testing mode. When testing microservices at the macro level, you need to ensure that there are two aspects to be satisfactory: interservice communication and deployment processes.
Ensuring intelligent interservice communication
Microservices are by definition independent of each other. They determine what to do based on the information they receive, so the accuracy of interservice communication is critical to the holistic operation of a microservice-oriented application.
Providing interservice communication means making sure that the right information comes where it needs to go and where it goes. Tests should be able to track how messages are exchanged and how they are processed. This is true for both HTTP request-response communication and the exchange of asynchronous messages propagated through a message broker. Testing should help ensure that message formats are supported to ensure a positive system path., and incorrectly formatted messages are rejected, and with sufficient explanations (and not just with a "bad message" error).
Continuous integration and continuous deployment testing
Well-organized continuous integration and continuous deployment (CI / CD) processes are essential in any software development paradigm, but when it comes to microservices applications, an efficient, accurate, and fast CI / CD process is critical. MOAs can be revised at a rate of hundreds of updates every day, so a single microservice that is late build can become a bottleneck and slow down the entire release process.
The best way to play it safe is to give CI / CD pipeline testing the same priority as any other high-level testing mode. Identifying and fixing issues such as slow build microservices due to artifacts in the code, slow provisioning of runtimes where microservices are hosted, and slow uplift of already deployed microservices are necessary prerequisites for the CI / CD pipeline to work. Even if the microservice is as complex as a shuttle, it will be of little use if you cannot quickly deploy and deploy operational units.
Conclusion
Microservices are the real wildcard. Microservice-focused applications provide the flexibility and speed needed to bring new features online at almost lightning speed. Large enterprises that support millions of users have learned this today, but every day more and more companies are adopting this architectural style when their applications reach the scale of the entire web.
While many companies are seriously trying to incorporate the spirit of a microservice-oriented architecture into their development processes, testing these MOAs often use practices that were originally intended for monolithic applications. This is a short-sighted approach.
On the contrary, companies must learn modern testing techniques designed to ensure the independence of microservices and the agility of the applications that use these microservices. When development teams and testers manage to synchronize the principles behind designing microservices applications, the entire company can leverage much more on the strengths of microservices.