Why restrict JUnit to unit testing? Why not test the full microservice? – Part 2
Part 1 of this article explored the options for testing a single service in a microservices application from the perspective of the developer of the service. In the article, I proposed an approach to address the main challenges of service testing. This second part of the article discusses how the proposed approach can be implemented using Junit and Mesh Dynamics.
Consider the set of services in a typical e-commerce application that take care of the order management functionality. To keep it simple, we consider two services as shown below: Order Receiver is the service under development, and Transformer is a producer (upstream) service for Order Receiver.
The goal is to test the functionality of the Order Receiver service without needing to test the entire application. As a developer of Order Receiver, I assume that the Transformer service works correctly without worrying about the following:
How Transformer is implemented
How Transformer is deployed
What producer services Transformer depends on
When Transformer’s APIs change, I should only need to address changes that are needed in the Order Receiver service to handle the changes in Transformer.
Many microservices application developers are adopting this strategy for testing their services independently. Service testing involves testing each service in isolation by mocking all the producer services. The service tests can be run pre-submit as well as in CI.
There are two workflows covered here. The first workflow focuses on testing the Order Receiver service independently. This workflow can be used by the developer of a service before submitting a change, as well as in CI.
The second workflow covers updating the service tests for the Order Receiver service when the Transformer service changes. Typically microservices developers always maintain backward compatibility so that the consumer services are not affected by changes to the producer services.
Service Testing: Testing a service independently
I can test the Order Receiver service independently by:
creating accurate mocks of the Transformer service APIs
writing Junit tests for Order Receiver
running service tests for Order Receiver with the Transformer service mocks
Learning producer API mocks
In order to test the Order Receiver service independently, we need to learn the API mocks for the corresponding requests to Transformer. This can be achieved by capturing the API request/response at the ingress to Order Receiver (Order Receiver’s API) and the corresponding requests/responses at the egress from Order Receiver (Transformer’s API).
Mesh Dynamics API Studio can capture both ingress and egress requests+responses for any service from the developer’s laptop. Egress requests are linked to corresponding ingress requests via tracing/observability.
Using this approach, I can create a collection of API traces that can both drive test requests to the Order Receiver service as well as mock the Transformer service. Let us name the collection GL_orderapp.
API Studio screenshot showing the captured trace where both the Order Receiver and Transformer services are running locally in an IDE
Write JUnit test case
Now let us write a JUnit test for Order Receiver’s APIs using the collection created above to mock the Transformer service.
If the service is written using Java Spring Boot, we just have to annotate the JUnit class with Mesh Dynamics annotations. The following annotations will configure the collection GL_orderapp to be used for mocking:
Now, let us look at a specific JUnit test case:
The test case above tests the Order Receiver service’s API with path “api/orders”. This API provides the business functionality of placing an order, and it depends on the Transformer service.
Notice that the test case is annotated with @MeshTestCaseId, with traceId and Order Receiver’s api path as attributes. The traceId value comes from the captured data (discussed in the previous section) and is used to select the specific Order Receiver request and response from the collection. This data is passed to the test method as MeshDRequest and MeshDResponse type objects. These parameters can be used to construct the request and assert the response. In the above example the request is completely constructed using the MeshDRequest request parameter passed.
Apart from providing MeshDRequest and MeshResponse objects, the traceId value is also used to select the correct response for the Transformer service mocks.
If you look at the JUnit test method above, it does not have any code to mock the Transformer service. And we are not running Transformer service locally or accessing any remote live Transformer service. So, where is the mock coming from?
The mock response is being served by Mesh Dynamics from the GL_orderapp collection we created earlier. As a developer, I did not have to set up the Transformer service locally. Nor did I write a bunch of code to mock the service. I focused only on testing my service while Mesh Dynamics automatically created the mocks needed for my service testing.
Changes to producer services
The second workflow covers updating the service tests for the Order Receiver service when the Transformer service changes. I will cover this in the next article.