In the industry, the architecture of a software product revolves around the components that make up the software, the way those components are structured, the relationships between them, and how they interact, communicate and coordinate with one another to improve the overall quality of the software. The goal is to develop and deliver a software product that will meet user demands and will be able to provide value to the users. To organize these different components and to structure the overall software architecture, different architectural patterns are used.
One of the architectural approaches which is gaining much popularity nowadays in the industry is the “Microservices Architecture”. In the simplest of terms, it is a way of designing the software as suites of de-coupled, cohesive, distributed, collaborative, independently deployable service components. A service component is a collection of modules which represent a single business purpose of the software application and can be easily and independently replaced, upgraded, and scaled. They are implemented, maintained, and deployed as separate independent units in a Microservices architecture.
How is a Microservices Architecture different from a Monolithic Architecture?
The word “Monolith”, from where the term “Monolithic” has originated, means “a single block consisting of several components put together”. Similarly, if an application is said to be based on “Monolithic Architecture”, it means that all the different components of the application are combined into a single program, tightly coupled with one another, and run as a single service.
The single unit can provide all or most of the business functionalities like populating user views, authorizing users, handling HTTP requests and sending responses, integration with 3rd party services or other external systems, execution of business logic, retrieval and update of data from one or more database instances and so on. The components usually communicate with each other using method invocation and function calls.
With the components bound together, the application is developed, tested, packaged, deployed, maintained, and scaled (by replicating many instances of it in multiple servers) as a single logical executable unit. Any change to any of the system components will require the deployment of the whole application.
On the other hand, a Microservices architectural style consists of several autonomous service components, each of which has its DB (it can be different instances of the same DB or entirely different DB technology) and provides a specific business functionality by using well-defined, lightweight service APIs through which it collaborates and communicates with other service components.
Most of the applications start with a monolithic architecture. But as they grow and evolve (maybe deployed to the cloud e.g. AWS, GCP, Azure) and seamless scaling of some/all of the functionalities becomes important, it makes sense to break down the application into smaller service components, each of which will possess the ability to provide different business functionality independently.
The service components can run their processes and collaborate using communication protocols. They can be implemented, tested, deployed, managed, and scaled by different teams. Also, each service component can be written in different programming languages or can use entirely different technologies/platforms.
Microservices – Protocols and Technologies
For a product, when it comes to applying different technologies for different needs by different teams, adopting Microservices architecture is probably the best approach. It is not a necessity that all of the components that make up the application should be created using the same technology, language, or platform. For each business problem, one language/technology/platform can be a better choice than the others.
Consider an enterprise-level application, for which the sales view for a customer can be written in Angular, the support view looks good in React, the report generation logic requires Java, the component creating analytics dashboard needs Python, and so on. If different technologies and languages are used for different services, then for inter-service communication, the use of standard message interchange protocols becomes necessary.
The components can communicate with each other using simple standard HTTP/REST with resource APIs or lightweight messaging protocols like AMQP (Advanced Message Queuing Protocol). The service components may interact with external systems (maybe from 3rd party) using JSON/XML over HTTP/REST. When it comes to collaborating with internal services, communication using language-agnostic, binary, text-based messaging protocols like gRPC (Remote Procedure Call) might be a good choice since it uses protocol buffers to specify service contracts.
Service contracts are the inputs and outputs defined by the services. To guarantee that the functionality of one service component (Consumer) is not broken if changes are made to another service component (Provider), service contracts are created between them.
Microservices Testing Challenges
While building a distributed system with Microservices has multiple benefits, testing such kind of application brings in its own set of challenges. Like testing monoliths using a traditional testing approach, if we try to test all of the service components in a Microservices together, in unison, in the same environment, the approach won’t work because most of the time all the service components may not be available in the required form, in the same environment and at the same time.
Also, as a “Provider”, one service component may not know about all the other service components which are “Consumers” of its APIs. Hence, after a change in its codebase, if it is tested and deployed to production, the probability is higher that functionalities of the consumer services will not be tested. When it comes to End-to-End testing, not only the collaboration between the different services will be difficult but also setting up an entire environment to run End-to-End tests will be quite costly and the automated tests will be too slow to provide feedback to the developers.
Doing exhaustive, non-automated testing during the build phases will also be difficult since the number of service components can be quite large as well as a better understanding of the services’ characteristics (e.g. in failure modes) is often obtained after observing and monitoring them in production, not in their development stages.
A Pragmatic Approach to Test Microservices
With these testing challenges, everything cannot be tested in a Microservices setup. We have to take a pragmatic risk-based approach to test the critical parts of the application code and Test Automation will be at the forefront of it. We have to make Test Automation central to the development process and we have to pick up the right subset of testing techniques as per the availability and reliability of the service components. Unit testing of the individual modules, Integration testing, Contract Testing of the service APIs, and End-to-End Testing can be adopted as some of the pre-production testing techniques to test the application but with defined goals, scope and tradeoffs.
Unit testing of each module, with the help of some small automated test suites, can focus on checking the behavior of that module by observing changes in its state, checking the interaction between the objects inside it and their dependencies, or checking the business logic which produces requests or map responses.
Integration testing will take the unit-tested modules together, form a subsystem, and test its integration/communication with other internal/external service components or systems. By using Unit Tests and Integration Tests, we will be able to achieve high coverage of the modules that make up the service components.
But we have to go beyond that and perform Contract Testing. Whenever some consumer component would want to couple with the interface of a provider component to use its output, a service contract will be created between them which will consist of expectations of input and output data. Contract tests can be used to verify that any change in the provider or the consumer component meets the service contract agreed between them. Some of the tools that can help us to perform Contract Testing are Pact, Pact.js, Spring Cloud Contracts. The automated test suite containing these contract tests should be integrated into the development build pipelines of the service components using some CI server (e.g. Jenkins). As a result, whenever changes will be made to any service codebase, then the developer will get quick feedback on whether her/his change is having an impact on the other services.
Finally, comparatively fewer automated End-to-End tests, which will cover some of the user journeys, can be run to verify that the complete application is meeting the business requirements.
Is your application based on a “Monolithic Architecture” or “Microservices Architecture”, and what is your approach to testing it? Share in the comments! 😉