Automated Testing Strategies in Microservice


Over the years, microservice get a lot of momentum in developing complex and distributed systems. It provides a lot of benefits such as independent deployment, scale, and maintains each service. But as it introduced more partition over the domains, the testing strategies are becoming more complex and diverse. 

In this article, I would like to describe some of the approaches we should consider on managing the automated testing in these distributed and independently deployable services and gain confidence over the development of a subset of the system. 

Why we need Automated Testing?

Firstly, in TDD (test-driven development), requirements of a system are being decomposed into test cases and, the development phase repeatedly validating those test cases to meet the business requirements. Adding the automated testing, we can ensure that all test cases passed for each development iteration. 

This automated testing also ensures the quality and the accuracy of the development. For each release of the service, it performs the same steps precisely every time they are executed and recorded the results in detail to analysis. 

Again, in the agile development process, to deliver things to live frequently, it is required to have great automated tests in the CI/CD pipeline. 

Components of Microservice

In a microservice architecture, the system components are organized around the business capabilities. It encapsulates a meaningful grouping of functionality and information for the independent delivery of business value. In most cases, the internal structure contains a layer of modules that separates the functionality. 

The resource layer exposed the protocol and resources of the service to the upstream service. Usually, these are lightweight and perform simple validations on the request and provide the response according to the outcome of the business transaction.

While most business logic resides in the domain model ( see DDD ), the repository layer collects the domain entities and often persists those data. The service layer coordinates across the domain activities and performs the actions for business requirements. 

The gateway layer encapsulates the message passing with the remote service In those cases, it is likely to use a client to handle the request-response. 

In the testing strategy, we need to consider each of these modules and communications. The automated tests should provide coverage for each of these modules at the finest granularity possible. 

Unit Testing

A unit test exercises the smallest piece of testable software in the application to determine whether it behaves as expected. 

Unit tests are typically written at the class level for each functional unit and verify its behavior independently from other parts. Unit tests are narrow in scope and allow us to cover all cases, ensuring that each unit is working correctly.

Benefits of Unit testing

  • Unit testing provides the validation on the unit of the feature. Any kind of refactoring can be validated at the earliest time of the development. 
  • By enforcing the code to be testable, unit testing usually leads to a cleaner design and better separation of concerns. 
  • Unit testing really goes hand-in-hand with agile programming of all flavors because it builds in tests that allow making changes more easily. In other words, unit tests facilitate safe refactoring. 

Code Coverage with Unit Testing

In microservice, the domain layer contains complex business requirements and state-based logic. Thus, the real domain objects should be unit tested. 

For the gateways and repositories, it is difficult to isolate the unit from the external modules and test against state changes. The unit test is being used to verify the logic on producing the requests or responses from external dependencies rather than to verify the communication in an integrated way. For these cases, using Test-Double (Dummy, Stubs, Fakes, Mocks, etc.) is more effective. 

Unit tests, in general, intend to constrain the behavior of the unit under test. But an unfortunate side effect is that sometimes, tests also constrain the implementation. That often leads to mock-based unit test approaches. So it is important to continuously examine the value a unit test provides versus the cost it has in maintenance. By doing this, it is possible to keep the test suite small, focussed, and of high value. 

Further reading:  The myth of 100% of code coverage 

The unit test alone does not provide guarantees about the behavior of the system. Although we can write a good coverage for each of the modules, the unit test does not provide coverage on the interaction with the remote dependencies. To verify that each module interacts correctly with its collaborators, more coarse-grained testing is required.

Integration Testing

An integration test verifies the communication paths and interactions between components to detect interface defects.

Integration tests demonstrate that different parts of the system work collectively as intended to achieve some larger piece of behavior. The integration tests are typically used to verify the interactions with external components to which the service is integrating like other microservices, data stores, and caches.

Benefits of Integration Testing

  • Allows ensuring the appropriateness of the modules in the service and their results.
  • Helps to detect the issues related to the interface between modules.
  • Covers multiple modules to provide broader test coverage.
  • Also helps to smoothen the transition between interfaces.

Coverage with Integration Testing

As the goal of Integration Testing is to cover basic success and error paths through the integration module i.e Gateway and Repository

Gateway integration tests provide the assurance that the protocol level errors such as missing HTTP headers, incorrect SSL handling, or request/response body mismatches are verified. 

Since the external services are distributed over the network, these are subject to timeout and network failures. To cover this kind of abnormal behavior, it can be beneficial to use a stub version of the external component as a test harness which can be configured to fail in predetermined ways.

Repository integration tests provide assurances that the schema assumed by the code matches that available in the data store.

Since most data stores exist across a network partition, they are also subject to timeouts and network failures. Integration tests should attempt to verify that the integration modules handle these failures gracefully.

Does unit and integration test gives enough confidence?

By combining unit and integration testing, we can achieve high coverage of the modules that make up a microservice and get the confidence that the microservice is working correctly for the required business logic.

The business value is not achieved unless many microservices work together to fulfill the whole business process. Within this testing scheme, there are still no tests that ensure external dependencies meet the contract expected of them or that our collection of microservices collaborate correctly to provide end-to-end business flows. We need more coarse-grained end-to-end testing of the whole system to help to provide this.

End-To-End (E2E)Testing

An end-to-end test verifies that a system meets external requirements and achieves its goals, testing the entire system from end to end.

The goal of end-to-end testing is to verify that the system as a whole meets the business requirements. The system is treated as a black-box and the test cover as much of the fully deployed system as possible.

Benefits of E2E Testing

  • Provides additional confidence in the correctness of messages passing between the services
  • Ensures any extra network infrastructure such as firewalls, proxies, or load-balancers is correctly configured. 
  • Allow a microservice to evolve over time. In the agile development process, as the domain is becoming more clear the services are likely to merge or split, or refactored. E2E testing gives the confidence that the business will remain intact during these architectural refactorings. 

Coverage with E2E Testing

The test boundary for E2E testing is much larger than any of the previous types of tests as it covers the fully integrated system. 

In most cases, systems have dependencies on one or more externally managed microservices. Usually, these external services are included within the end-to-end test boundary. However, in rare cases, one may choose to exclude them.

  • If an external service is managed by a third party, it may not be possible to write end-to-end tests in a repeatable and side-effect-free manner. 
  • Similarly, some services may suffer from reliability problems that cause end-to-end tests to fail for reasons outside of the team’s control. 

In cases such as these, it can be beneficial to stub the external services, losing some end-to-end confidence but gaining stability in the test suite.

Since end-to-end tests involve many moving parts they also have many factors to fail from time to time. To manage the additional complexity for the end-to-end tests we may consider to

  • write only the valuable tests and core business scenarios
  • focus on the user journeys
  • make tests data-independent 

Conclusion

In a microservice architecture, the system is divided into small well-defined services defined by its bounded context. These bounded contexts provide opportunities and flexibility in terms of the type and level of testing that can be applied. 

In some cases, a microservice may encapsulate a central business process with complex requirements. The criticality of this service may require extensive testing of the service – unit testing, integration testing, end-to-end testing

In other cases, a microservice may be less critical from a business standpoint or may have a short lifespan. In those cases, the level of test coverage required may be lower, and only unit testing can be enough.

Leave a comment