Testing Microservices Applications
- Posted by Yomna Anwar
- On June 8, 2021
In the software industry, the architecture of a software product revolves around the components that make up the software, how they are structured and how they communicate with each other to meet user demands and provide them with an added value.
One of the architectural approaches that is gaining much popularity nowadays in the industry is the “Microservices Architecture”. As its name suggests, it is mainly based on dividing your system into smaller services, each responsible for specific functionality and can be accessed or connected with other services and components through lightweight APIs to business valuable features.
Testing microservices-based applications can have its own sets of challenges, such as selecting the right testing activities when to perform testing and the scope of each testing level. In this article, we will be discussing different testing levels applied in microservices applications and their different approaches.
Microservices display a similar internal structure consisting of all or some of the displayed layers below.
To simplify the above figure, there is a microservice that can be reached through a certain endpoint (Protocol Layer) to perform some function, this function is done by performing a certain logic (Domain Layer), this logic can sometimes need to interact with Database whether to read or update (Persistence Layer) and may also need to interact with external service (External Layer).
The testing strategy followed and testing levels applied should aim to provide coverage to each layer and between layers of the service whilst remaining lightweight. Testing automation pyramid followed in microservices systems aims to focus testing activities at lower levels and decrease as we go up.
Unit Tests Level
Unit tests can be used to verify any logic used and provide fast feedback about the core of the microservice and if it provides the intended business value or not. Unit tests mainly target the Domain layer mentioned in Figure 1.
Tests are written using unit testing libraries such as JUnit, mockito, etc. and directly call the implementation methods. They do not need to start the service locally or hit the service via its endpoint.
The main challenge in unit tests level is to determine the size of the unit under test since it can vary from a service to another, however, the rule of thumb is as small as possible and as large, as necessary. Having a test-driven mindset while developing a service layer to have a testable code can be a great asset to selecting units under test.
Unit tests implementation is mainly a developer task, a skilled QA can participate in it, but QA main task at this level is providing test scenarios that will cover service logic.
Component Test Levels
After verifying the different functions or units forming the microservice are working correctly, we need to check the component formed from these units & functions is performing correctly or not. In microservices systems, components are the microservices, to test this component we start hitting its endpoint before integrating with real external components – if there is any- and checking its response.
Here we need to isolate each component (service) from its peers or collaborators by using test doubles. We should mock all the external dependencies and test the service in isolation. This can introduce some challenges in the test setup to return predefined responses when certain requests are received, in addition to implementing in-memory datastores.
Tests communicate with the microservice through an internal interface allowing requests to be dispatched and responses to be retrieved. In this way, tests can get as close as possible to executing real HTTP requests without real network interactions.
The scope of this level can vary based on your implementation, the common case will be covering different responses or response codes that the endpoint can respond with, in addition to minimal functional testing. Some other times on this level, heavy functional testing can be performed, which is not always favourable since these tests are usually slow compared to unit tests.
Component tests implementation can be assigned to a developer or QA, given that he has the required skills.
The question that arises here is since we have verified the unit logic is working correctly, and calling the service through its endpoint is also verified, should we consider this microservice done and move to the next one?
The answer is no, we still need to verify how will this service behave when it’s integrated with real components, this can be covered at the integration tests level.
Integration Tests Level
Integration tests collect modules together and test them as a subsystem to verify that they collaborate as intended.
The main goal is to verify the communication paths and interactions between services and external components to detect interface defects. This can be achieved by focusing on making sure that modules can connect successfully rather than performing a functional test on them.
Integrations tests implementation can be assigned to a developer or QA, given that he has the required skills.
For example, if your microservice function is to retrieve a list of users from a database, integration tests should cover the format of response body parameters agreed and response status codes.
E2E Tests Level
While developing a microservices application, one can get distracted away from the overall purpose of the application, which is when E2E test scripts come to the rescue.
Microservice function and response on the backend – based on its scope – can be displayed for the user through a developed UI that will have its testing activities on its own. E2E scripts do not aim to find bugs, however, they aim to give us confidence that this part of the application provides its business value for the end-user.
Exploratory Tests Level
After we integrate microservices together, making sure they work together correctly and the application provides the right business values, some defects can still be found with the application.
On this level the main scope is to make sure that the application is usable to end-users, there are no UI issues that usually cannot be caught with the previous automated tests or any business gaps that were missed during the different test pyramid activities.
Testing Microservices Applications vs Monolithic
Monolithic applications are built as one giant software that contains all logic and services within the same package, microservices applications in contrast are formed of small units or services that can be integrated together to provide the final application.
Due to microservices applications nature and architecture, and since it depends on small components talking to each other, most testing activities are performed at lower levels, to make sure that these components are working fine on their own (Unit & Component levels) and when integrated with other services and components (Integration level).
The testing perspective is kindly different in a monolithic application, testing activities are still performed at lower levels, but mainly testing activities are focused on higher levels, especially the system and E2E tests level, which matches the nature of monolithic applications architecture as it is delivered as one package including all.
Conclusion
Shifting testing activities and selecting the right testing strategy to match technologies and architecture pattern used with an application under testing can be quite challenging and have different variations, metrics should be set to make sure testing activities are effective and overall quality is maintained.