Microservices are a popular architectural style for building applications that are resilient, independently deployable and highly scalable. But a successful microservice architecture requires different approach to both designing and testing.
What is a Microservice architecture and how it differs from monolithic architecture?
A microservices architecture consists of a collection of small, autonomous services. Each service is self-contained and should implement a single business capability within a bounded context. Multiple services combine together to as a system to provide business valuable features. Below are some key features:
- Self-contained and componentized: Microservices are small, independent, and loosely coupled. Each service is a separate codebase.
- Services can be deployed independently. Changes made by one team does not require rebuilding and redeploying entire application.
- Decentralized data management: Services are responsible for persisting their own data or external state. This differs from the traditional model, where a separate data layer handles data persistence.
- They communicate with each other by using well-defined APIs.
- Services do not need to share same technology stack, libraries or frameworks.
"Problem Statement – Testing strategies in microservice architecture-based application"
Challenges in Testing Microservices
Microservices are typically developed by small team working on different technologies and frameworks. They are integrated using well defined API calls. Testing teams generally might be inclined to use web API based traditional testing approach. This may not be a good approach as all services may not be ready at the same time. Another challenge is individual service should be capable of working independently. Thirdly, data management are decentralized and hence traditional approach may not fit. Services can also, integrate with externally exposed services or exposed to customers as a service in that case. Also testing resilience of the service when dependent service is failing and vice-versa required separate thought process.
Testing Strategies in a Microservices application
In order to overcome the key challenges as described above, we need to first identify key types of testing. We should keep in mind Mike Cohn’s Testing Pyramid as shown in the image below and corelate with and find out the amount of testing required. Key testing types that we need to do are: Unit Testing, Component testing, Contract testing, Integration testing, End to End UI testing. Generally, we skip contract testing and move to Integration and end to end. Also, proper emphasis should be given to unit testing.
This is where bulk of the testing should be done. It ensures that each service works as expected in isolation. These tests can include tests for input validation, boundary conditions, and error handling, among other things. Unit tests should be written in such a way that they can be easily 100% automated and run as part of a continuous integration/continuous delivery (CI/CD) pipeline.
A component is a microservice or set of microservices that implement a role within the larger system. Component testing allows to independently validate and evaluate the performance of each component of a microservice application without integrating other services.
Generally, it is easier and faster to run component tests. Other than finding issues early, component testing can also help to ensure that microservices adhere to the desired architecture and design principles, such as loose coupling and high cohesion.
There are several tools and frameworks available for component testing in microservices, such as TestContainers, WireMock, and Hoverfly. These tools provide a range of features, including mocking, service virtualization, and API testing, which can help developers to create comprehensive and effective component tests.
This type of testing focuses on ensuring that two different software components or services can communicate with each other effectively and efficiently by verifying that they conform to an agreed-upon contract or interface.
A contract defines the expected input and output of a component or service. It can take many forms, such as an API specification, a message schema, or a set of database constraints. Once the contract is established, contract testing tools are used to verify that the services adhere to the contract.
It can help catch issues early in the development process, reduce the risk of integration failures, and make it easier to refactor or modify code without breaking downstream dependencies. Additionally, contract testing can improve the reliability and maintainability of distributed systems by ensuring that all components adhere to a common contract.
Some of the tools and frameworks available for contract testing, including Pact, Spring Cloud Contract, and Consumer Driven Contracts (CDCs).
It is a type of testing that verifies that individual microservices or components can work together effectively in a larger system or application. It is a critical step in the testing process, as it ensures that all of the microservices interact correctly with each other and with any external services.
It identifies issues such as incorrect data formats, communication failures, and performance bottlenecks that may arise when multiple microservices are combined. It can also help to ensure that the overall functionality of the application is achieved, and that it meets the desired business requirements.
Tools and frameworks available for integration testing in microservices, including Karate, RestAssured, and WireMock. These tools provide a range of features, including API testing, service virtualization, and load testing. This tools can help developers to create comprehensive and effective integration tests. Additionally, many microservices testing frameworks, such as Pact and Spring Cloud Contract, support integration testing as part of their testing suite.
End to End UI testing
Testing at this stage should be minimal since we have ideally thoroughly tested earlier during unit testing, contract testing and integration testing. This testing should be more from end user point of view. Any defect found at this stage will be difficult to find and fix and will be costly. This testing method simulates user actions and tests how all the microservices interact with each other. Hence, ensuring that the final application delivers the desired user experience. Automated UI testing frameworks, such as Selenium and Cypress, can be used to simulate user interactions and test the entire application workflow.
Testing Scenarios in Microservices applications
Other that the different types of testing, we can also list out some common key scenarios in such applications:
- Testing between microservices internal to an application – This is a most common scenario. We can consider an example of e-commerce application. (i) search item and (ii) Buy/add to cart an item can be considered as 2 individual services. They closely interact with each other.
- Testing between microservices and third-party services – In this case a service in the application consumes or interact with external API. In the above example we can think of a (iii) Payment for an order as microservice which interacts with UPI payment API.
- Testing microservices that is to be exposed to public domain – Here we can take example of e-commerce B2B application where retailers can check for availability of an item by invoking Web API.
Thus we see that with the change in software architecture there is a definite requirement to change in testing strategy and approach and adapt accordingly. Thus testing teams also need to keep themselves informed and educated on the latest tools and strategies. It can help to deal with the challenges imposed. Also, we have seen that its better to mitigate risk earlier in the development cycle and do bulk of test as soon as possible during unit, component and contract testing.