Testing gRPC #3: How to unit test a gRPC client

Gaurav Singh
8 min readMar 7, 2024

--

⏪ Recap:

In the previous blog, we understood how to write a unit test for a gRPC server using InProcessChannel, and InProcessServer and how to set automatic cleanup using GrpcCleanupRule. In case you missed it, please feel free to read it here

⏩ What we’ll learn:

In this blog, we’ll understand how a client could be written for our Grpc service and also how can we unit test that.

Let’s go ⚡

System under test

Before we understand how the client is written, let’s first grasp how the service is implemented for this method since we have to write a client method for essentially this.

You can see the complete listFeatures() method in RouteGuideServer.java below:

Let’s walk through the code and grasp how this server method is implemented

If you observe the method signature, We pass in a Rectangle request and StreamObserver<Feature> responseObserver

We know from route_guide.proto, that Rectangle is a collection of 2 Point (lo and hi), such that each point has a latitude and longitude. This represents a bounding rectangle in a map. Think of a rectangle that covers the central part of Bangalore (of course, there could be much better representation to create a geofence but for the sake of simplicity let’s go with this 😏)

Below is the proto in case you need a recap.

route_guide.proto

We also pass in StreamObserver<Feature> responseObserver which is used to return a stream of Feature objects to the client. The stream could loosely be understood as an array that the client can read either all at once or one object at a time.

Let’s look at the service methods body, We then extract 4 corners of the rectangle by finding min and max values from lo and hi longitude and latitudes to get an integer that represents each corner.

Nice, We then iterate in the features already stored in our database (in this example, the features are stored in an in-memory Collection but in a real application this could be any other data store) and check if a feature exists

If you see the exists() it just checks if the feature has a valid name

If the feature exists then we check

  • if that feature longitude lies between left and right ranges
  • and latitude lies between the bottom and top ranges

if we find such a feature then we add it to the responseObserver’s onNext method

Finally, we call the responseObserver.onCompleted(); method to indicate that the rpc call is complete.

How to Implement a Client

So now we understand what our service is actually doing.

Let’s understand how a client method could be written for listFeatures() method

Any client for a service method/operation usually performs 3 functions:

  • Construct the request for the service
  • Call the service method using either a blockingStub or an asyncStub(in other words either sync or async call)
  • Logs or return the response back for further processing

With above context, We can write a gRPC client for this listFeatures() method like below

Initialize client

Let’s unpack the client and understand how it works

We start with Initialising our client class RouteGuideClient and setup our logger using standard java.util.logging.Logger;

Next, We declare a RouteGuideBlockingStub and a RouteGuideStub to make desired sync or async calls to the server and also initialize few other utilities like Random and TestHelper which may be used

We then pass in the channel to the client constructor and then initialize our blocking and async stubs using method from RouteGuideGrpc

listFeatures

We implement our listFeatures client method that accepts 4 int parameters, as we saw before they represent the co-ordinates for bottom left and upper right corners of the bounding rectangle

  • lowLat → Latitude for a lower point of the rectangle
  • lowLon → Longitude for a lower point of the rectangle
  • hiLat → Latitude for the upper point of the rectangle
  • hiLon → Longitude for the upper point of the rectangle

For our client, we prepare our request by constructing the Rectangle object that the service expects. We can directly use the newBuilder() exposed for each proto and use builder pattern to construct our request

Next, we make the actual request to get our list of features using blockingStub and store it in a collection called Iterator<Feature> that we can iterate upon

We then iterate and log each feature found within the specified coordinates. If we catch a StatusRuntimeException then we log this as a warning

In a real application, the client might do further processing based on these features but for now, we just log the features received

How to unit test a client

So now, we understand the basics

  • How the service method under test is implemented
  • How is the client for such a service written

Let’s come to the fun part.

How will we ensure our client works fine?

Of course, we write a test

You can find the complete test examples/src/test/java/io/grpc/examples/routeguide/RouteGuideClientTest.java but we’ll focus on how to write a test for listFeatures()

}

I know, its a lot. 🤟

Let’s break it down step by step and grasp how this works

Test Structure

The code is a test class named RouteGuideClientTest designed to test a client for communicating with a service called RouteGuide. It uses JUnit4 as we saw before

Automatic Cleanup

  • The @Rule annotation tells JUnit to manage an instance of GrpcCleanupRule.
  • This rule ensures that test servers are automatically shut down after each test, keeping the test environment clean.

Setup

We write @Before annotated setUp method to ensure each test method starts with a unique instance of the client

We create a unique in-process server (running within the same process)

We also use a registry to allow different services to be registered for different test cases.

We then initialize the client and pass it the channel which is also registered for auto cleanup

We inject a mock object called testHelper for observing calls and verifying behavior. We’ll see that this is an interface that exposes

Tests

We then write 2 unit tests that do below on a high level:

  • listFeatures: This tests successful communication with the server simulating a positive case
  • Creates a fake service implementation to control server behavior.
  • Calls the client’s listFeatures method to initiate communication.
  • Asserts that the correct request was sent and the expected responses were received.
  • listFeatures_error: Tests how the client handles errors.
  • Set up a fake service that throws an error.
  • Verifies that the client properly propagates the error to the test helper.

✅ Positive case

Let’s unpack the positive case first to understand this a bit better

Below is the complete test at a glance. We will do a walkthrough on it below:

Alright, let’s break this down.

We first annotate our method with @Test and then build two feature objects by using the builder as before by passing in the name as “feature 1” and “feature 2”

We create a thread-safe reference to the Rectangle object using Java AtomicReference

In this test, we are only interested in testing our client, thus we create a fake service implementation for listFeatures method. This ensures isolation for this unit test.

To do so, we construct new RouteGuideImplBase()and define our anonymous subclass by overriding the listFeatures method like below

Inside the body, we set the rectangle passed in from the request into the AtomicReference<Rectangle> rectangleDelivered we had defined earlier

We also want the server to stream and return the two features we had earlier created to the client, thus we use the onNext() method in responseObserver to do so. This is a way for gRPC to provide server-side streaming

Finally, we complete the RPC by calling onCompleted() method

We can see the complete fake service implementation below.

Now, we will add this fake service to our service registry as below

Awesome, Let’s make our service call via the client. Here 1, 2, 3, and 4 are the lat and longs for the lower left and upper right corner of the rectangle as we saw before.

After making the call, we should check whether the rectangle object returned from the response matches what we expect with the below

Lastly, we also need to verify if our client actually called our fake service. We can ensure that using verify() method from Mockito library

We use the verify() method from Mockito library to verify interactions with the mocked object testHelper as below

This interface is defined in RouteGuideClient.java as below and if we go over its definition, it exposes two methods onMessage() and onRpcError. We can use this to ensure the client calls the onMessage() method with responseFeature1 and then with responseFeature2

We also ensure that the onRpcError() method was never called since this is a positive case.

You can see other examples of verify() and never() method calls and their usages on mockito docs

⛔ Negative test

How would we write a negative test for our client?

In this test, We want to check what happens when our service throws an RPC error

The initial setup is identical, except in this case we only prepare one responseFeature1

We initialize a fakeError of StatusRuntimeException type and then initialize it with an INVALID_ARGUMENT value

We again implement our fake service and repeat the same steps as above:

Notice, the last step. 🔵

We now use the onError method to return the fake error that we had created earlier. In a production app, the service may use this to communicate to the client that something went wrong in processing.

We then register our service, call our client with the same input, and assert that the API returns us the similar output by asserting on the rectangle object

To verify the service works for our negative scenario

We perform below 4 steps:

  • Set up a capture mechanism for errors.
  • Verify that a message was received by the service (likely before the error).
  • Verify that an RPC error occurred and capture the specific error.
  • Assert that the captured error’s status matches the expected error status.

Let’s see how this could be implemented using the Mockito library

We check that the onMessage() method of the mocked object testHelper was invoked with the specific argument responseFeature1

We define the ArgumentCaptor class from Mockito to capture any exception of type Throwable to our mocked method

Then, we ensure that onRpcError() method of mocked testHelper is called and by using capture() method of ArgumentCaptor, we capture the error returned by the GRPC service

We also assert that the captured error status matches expected fakeError.getStatus()by retrieving the captured Throwable using errorCaptor.getValue() and convert it into a GRPC Status object using Status.fromThrowable()

Conclusion

Let’s recap what we learned in this post:

  • We understood how listFeature() method is implemented and how can a gRPC server return a streaming response
  • We understood how the client method is written for this service
  • Later, we dove into how can we unit test the client
  • We looked into how to write a fake service
  • We implemented a positive case and checked if calls were made using Mockito verify()
  • We implemented a negative case and then checked if error was returned using Mockito ArgumentCaptor

That was lot of ground 🌆, and this wraps up what I wanted to cover with unit testing. There are other aspects to it but I’ll leave that to you to explore with other methods.

Please let me know if you have questions or thoughts in the comments.

In the next post, we will grasp how to write a functional API test for this to get confidence that the service works when dealing with live data.

Thanks for the time you spent reading this 🙌. If you found this post helpful, please share it and follow me (@automationhacks) for more such insights in Software Testing and Automation. Until next time 👋, Happy Testing 🕵🏻 and Learning! 🌱

NewsletterYouTubeBlogLinkedIn

Twitter.

Originally published at https://automationhacks.io on March 7, 2024.

--

--

Gaurav Singh

Software Engineer passionate about Software Testing, Test Automation {Tooling, Frameworks, Infrastructure}, and scaling teams.