7 March 2023
05 Min. Read
Testing Microservices: Faster Releases, Fewer Bugs
Fast Facts
Get a quick overview of this blog
The inter-dependency between services proves to be a challenge in testing.
Unit testing fails because its limited to test function-level scope only.
While E2E tests fails because they fail to identify the root cause of failure.
Record and test approach works best for testing these multi-repo architecture.
As microservices gain widespread adoption, various challenges arise, such as unexpected functional errors, inter-service dependencies, and difficulties in identifying the main reasons for failures. It is clear that testing microservices is a complex task, and engineers have been actively searching for a definitive solution to address these issues once and for all.
The idea of "More APIs with smaller responsibilities" in microservices aims to address problems in tightly-packed monolithic architectures. However, this approach also introduces more potential points of failure, making microservices more vulnerable to errors.
We have developed a solution that can help you test your services without worrying about breaking changes creeping in all the time. In this blog post, we'll introduce a unique approach to testing microservices. This method has the potential to save you hours of debugging time and boost your team's confidence in releasing updates, ensuring that you won't ever need all your services up and running to test the interactions between them.
What’s the hard part about testing microservices?
Teams that work on a shared repository design excel in collaborative development, benefiting from shared resources and knowledge. However, they may encounter challenges related to testing and speed, leading to suboptimal outcomes. Few reasons that can cause these:
Dependency Management Complexity: When the API specification of a service X changes, it necessitates updating the corresponding specification in the repository of service Y (on which X depends), and similarly for all other interconnected services. This process can be cumbersome and time-consuming, affecting the development flow.
API Definition Synchronization: Keeping all repositories updated with the latest API definitions is not straightforward. Development teams must meticulously collaborate to ensure precise synchronization, as any oversight may lead to code breaks in production, causing disruptions and potential downtime.
Testing Bottlenecks: With multiple teams working on a shared repository, testing can become intricate. Changes made by one team may inadvertently impact the functionality of other services, resulting in increased testing efforts and potentially more bugs slipping into the production.
Speed and Efficiency Implications: As the shared repository grows larger and more complex, the development process may become less agile. Longer development cycles and slower iterations could hinder the overall speed and efficiency of the development workflow.
And since microservices are always interacting with each other to complete business logic, the interaction interface becomes too complex to handle as the application grows in size. And as we know by now, E2E tests doesn’t really fit into testing microservices well, developers opt for testing these interactions on a unit (smaller) level first.
The Problem with low-level Unit Tests🔴
Challenges of testing microservices doesn't end here; developers spend a lot of time writing and maintaining unit (integration) tests for their services. The problem is that these handwritten tests need constant maintenance as the service evolves. This kills productive man-hours and, as a side effect, slows down release velocity.
Low-level unit tests written by developers can only test input and output for functions. But testing remains incomplete unless code is tested with all its external components, i.e., databases, downstream systems, asynchronous flows or queues, and filesystems.
How to perform microservices testing with an easy approach?🤩🚀
HyperTest has developed a unique approach that can help developers automatically generate integration tests that test code with all its external components for every commit.
It works on Real-time traffic replication (RTR), which monitors real user activity from production using a SDK set-up in your repo and automatically converts real-world scenarios into testable cases. These can be run locally or via CI to catch first-cut regressions and errors before a merge request moves to production.
👉 HyperTest’s Record Mode
During the recording phase, all the requests coming to the service under test (SUT) are monitored while capturing all the interactions happening between its components. This typically involves capturing inputs, outputs, and communication between different modules or services. The recorded data can include function calls, network requests, message exchanges, database queries, and other relevant information.
HyperTest’s SDK sits directly on top of SUT, monitoring and recording all the incoming traffic requests received by the service. It will capture the complete flow of actions that the SUT follows to give back the response.
The incoming requests are the user flows that are recorded as-is by HyperTest. They are separated into distinct test cases using configurable identifiers like authentication cookies, headers, etc.
Most of the common scenario’s involve the SUT talking to databases, downstream services, or sometimes any external third-party APIs or cloud services to generate the desired response for a specific request.
HyperTest’s SDK records the complete user flow, be it the service making any outbound requests to the database or using that response to make an outbound request to any downstream service to finally fulfill the incoming request made to the service. It eliminates the need to depend on external dependencies while testing a scenario.
Recording a user session that can be used as a test case is very simple. HyperTest records hundreds or thousands of such different flows to build a powerful regression suite in minutes.
👉 HyperTest’s Replay/Test Mode
During the (replay) Test mode, integrations between components are verified by replaying the exact transaction (request) recorded from production during the record mode. The service then makes external requests to downstream systems, databases, or queues that are already mocked. HyperTest uses the mocked response to complete these calls, then compares the response of the SUT in the record mode to the test mode. If the response changes, HyperTest reports a regression.
Under the test mode of HyperTest, the HT CLI (Command Line Interface) requests data from the HyperTest servers that was captured in the record mode. This data includes the requests, responses, and mocks (in our example, stored response: X’).
Once the HT CLI has the stored response, it sends a request to the SUT to test its response.
This time, the requests to the database and the downstream services will not be made. Instead, the previously captured requests and responses are played back, completely eliminating the need to keep the dependencies up and running.
The HT CLI now has one new response (the new test response: X’’) and one stored response (X’). It compares these two responses to identify any discrepancies, such as changes in status code, content type, schema, data, and so on.
If the dev who owns this service approves the changes, all devs who own services that are dependent on the SUT are notified on Slack of the breaking change.
Optimize your Microservices Testing with HyperTest🤝
Because microservices architecture requires individual deployability, it becomes challenging to test them collectively before release. This raises important questions:
How can we be confident in testing them together when they're assembled as a functional system in production?
How can we test the interactions between these units without requiring all of them to be operational simultaneously?
But with HyperTest’s approach, you can remove the pain, planning and drain of building, testing and deploying micro-services. Book a demo today and empower your development teams to locally test all changes using HyperTest. Ensure changes don't disrupt dependent services, ensuring a seamless, error-free release cycle.
Related to Integration Testing