Testing gRPC #1: Set up a gRPC server and make an API call via gRPC UI
Hi everyone,
I’ve been busy testing and automating some gRPC APIs at work for the past 6 months and I’ve found this technology fascinating.
While HTTP APIs are discussed extensively in blogs and conference talks, curiously gRPC is still a less talked about technology in the Quality engineering space. Let’s change that, shall we?
Why should you read this? ❓
In this blog series, we’ll take a deeper look at gRPC from the lens of a Software Engineer looking to test all the different aspects. If your org uses gRPC this may help solidify its concepts and help you to test better. If not, who knows you may come across this technology in the future and can come back to this series
What will we learn in this series?
- gRPC concepts such as client stub, protocol buffers, blocking, and async calls
- Set up a gRPC server and a client for an example service
- How to do unit testing
- How to do functional API testing
- Performance test these APIs
By the end of this series, I want to personally have a deeper intuition of how to approach all testing at these different layers and take you along with me on this learning journey.
This will be a ton of fun 💖.
I hope you are as excited to start on this learning journey with me. 🏃
What is gRPC?
gRPC stands for Google Remote Procedure call, It is a modern framework to design APIs so that a client can call a method with some params on a remote server (running on a different machine) as if it were a local object.
gRPC uses protocol buffers to define the service and enable serialization/de-serialization. It is essentially an interface definition language (IDL) and we can generate client and server code in any supported language (C++, Java, Python… etc) using a proto compiler.
Source: Introduction to gRPC
Why gRPC?
- Clients (stub) could call any method defined within a service
- gRPC is language agnostic i.e. clients could be written in one language (Ruby, Java) and served in another one (C++)
- gRPC allows for full duplex streaming i.e. client streaming, server streaming, or bi-directional streaming of messages such that clients and servers can issue read or write streams in any order.
- It supports sync and async flows
- Mobile Clients can use advanced streaming and connection features that help save bandwidth and make fewer TCP connections to save battery life and CPU usage
- gRPC provides advanced load balancing and failover, cascading call cancellation, interaction with flow control at the application layer
- Uses static paths in URLs for performance reasons and reducing the cost of parsing query, path, and payload body
- Has formalized set of errors that are more directly applicable to APIs than HTTP status code
If you prefer watching a video, you can watch from ByteByteGo for a quick overview or from IBM Cloud
Protocol buffers
Protocol Buffers (protobufs) are used as the IDL (Interface definition language) to define the gRPC service, essentially this means we can define what methods would this server/API expose along with what params would be passed in, and what response would be returned.
Protobufs are also used as the message exchange format to define the request/response formats and then using a proto compiler (protoc), we can generate client and server code in the supported languages.
What are some advantages of using protocol buffers?
- Compact data storage
- Fast parsing
- Language and platform-neutral
- Extensible data format for structured data
- Protocol buffers support the addition and deletion of fields without breaking any existing service
How are protocol buffers used?
Source: How do protocol buffers work
For example: below Person.proto generates below Java class Person.java
Person.proto
message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;
}
Person.java
Person john = Person.newBuilder()
.setId(1234)
.setName("John Doe")
.setEmail("jdoe@example.com")
.build();
output = new FileOutputStream(args[0]);
john.writeTo(output);
To generate client and server code using protoc (protocol buffer compiler), Please read grpc-java/README.md at master
Setup
Now that we have some idea about gRPC and protocol buffers (at least conceptually), let’s get our hands dirty 🤝 and set up a gRPC server and client for an example service to see how these work practically
We will use the Java programming language and I’ll be using Java 17 in this example, we will follow the official gRPC tutorial to set up this app with the client and server
The service that we will set up is called the route guide. You can find the code for that in /grpc/examples/routeguide, we will understand the methods it offers as we set it up.
Let’s clone the code using the below
git clone -b v1.61.0 --depth 1 https://github.com/grpc/grpc-java
Note:
We use the v1.61.0
branch by specifying -b
in the clone command and –depth
to do a shallow clone and only get the latest commit. This is to ensure we don’t pull in all the git history
We will change the directory to
cd grpc-java/examples
Defining service and request, response in proto file
The service request and response types are specified in the proto file https://github.com/grpc/grpc-java/blob/v1.61.x/examples/src/main/proto/route_guide.proto
To see the service definition, its methods, and request responses, let’s look at the service RouteGuide
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.routeguide";
option java_outer_classname = "RouteGuideProto";
option objc_class_prefix = "RTG";
package routeguide;
// Interface exported by the server.
service RouteGuide {
// A simple RPC.
//
// Obtains the feature at a given position.
//
// A feature with an empty name is returned if there's no feature at the given
// position.
rpc GetFeature(Point) returns (Feature) {}
// A server-to-client streaming RPC.
//
// Obtains the Features available within the given Rectangle. Results are
// streamed rather than returned at once (e.g. in a response message with a
// repeated field), as the rectangle may cover a large area and contain a
// huge number of features.
rpc ListFeatures(Rectangle) returns (stream Feature) {}
// A client-to-server streaming RPC.
//
// Accepts a stream of Points on a route being traversed, returning a
// RouteSummary when traversal is completed.
rpc RecordRoute(stream Point) returns (RouteSummary) {}
// A Bidirectional streaming RPC.
//
// Accepts a stream of RouteNotes sent while a route is being traversed,
// while receiving other RouteNotes (e.g. from other users).
rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}
}
We can see that this service offers 4 different methods
- GetFeature
- ListFeatures
- RecordRoute
- RouteChat.
Each of these provides an example of a different type of RPC call that we can make (simple, server side streaming, client side streaming, bidirectional streaming).
We will grasp them when we test these methods, but for now, you can see that each method accepts a proto and returns either a single proto or a stream of protos (array of protos)
If we look further down in the proto file, we can see how these protos are defined.
- Each proto starts with a keyword message
- We specify fields with their appropriate data types like int32, string, or repeated (similar to an array)
- Proto can have other proto messages as its member fields to create a rich representation of data
// Points are represented as latitude-longitude pairs in the E7 representation
// (degrees multiplied by 10**7 and rounded to the nearest integer).
// Latitudes should be in the range +/- 90 degrees and longitude should be in
// the range +/- 180 degrees (inclusive).
message Point {
int32 latitude = 1;
int32 longitude = 2;
}
// A latitude-longitude rectangle, represented as two diagonally opposite
// points "lo" and "hi".
message Rectangle {
// One corner of the rectangle.
Point lo = 1;
// The other corner of the rectangle.
Point hi = 2;
}
// A feature names something at a given point.
//
// If a feature could not be named, the name is empty.
message Feature {
// The name of the feature.
string name = 1;
// The point where the feature is detected.
Point location = 2;
}
// Not used in the RPC. Instead, this is here for the form serialized to disk.
message FeatureDatabase {
repeated Feature feature = 1;
}
// A RouteNote is a message sent while at a given point.
message RouteNote {
// The location from which the message is sent.
Point location = 1;
// The message to be sent.
string message = 2;
}
// A RouteSummary is received in response to a RecordRoute rpc.
//
// It contains the number of individual points received, the number of
// detected features, and the total distance covered as the cumulative sum of
// the distance between each point.
message RouteSummary {
// The number of points received.
int32 point_count = 1;
// The number of known features passed while traversing the route.
int32 feature_count = 2;
// The distance covered in metres.
int32 distance = 3;
// The duration of the traversal in seconds.
int32 elapsed_time = 4;
}
Generate server and client code
Given the above service definition, we usually generate client and server interfaces using proto proto-compiler
In the above repo, we can use Gradle to generate these Java classes. You can find these instructions at https://github.com/grpc/grpc-java/blob/master/README.md
We need to ensure the below dependencies are added to our build.gradle file
runtimeOnly 'io.grpc:grpc-netty-shaded:1.61.0'
implementation 'io.grpc:grpc-protobuf:1.61.0'
implementation 'io.grpc:grpc-stub:1.61.0'
compileOnly 'org.apache.tomcat:annotations-api:6.0.53' // necessary for Java 9+
We need to have below config in our build.gradle file to allow generation of client and server interfaces
plugins {
id 'com.google.protobuf' version '0.9.4'
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.25.1"
}
plugins {
grpc {
artifact = 'io.grpc:protoc-gen-grpc-java:1.61.0'
}
}
generateProtoTasks {
all()*.plugins {
grpc {}
}
}
}
Note: grpc-java adds config to support both Android and non-android projects and thus the config is slightly more complex. You can see it here
To ensure latest classes are generated, we should run:
cd examples
./gradlew installDist -PskipAndroid=true
Running server with reflection and seeing APIs with gRPC UI
And to run the server, run below:
./build/install/examples/bin/route-guide-server
We can now see that we have the server running on localhost:8980
Feb 12, 2024 10:36:08 PM io.grpc.examples.routeguide.RouteGuideServer start
INFO: Server started, listening on 8980
Let’s play with these APIs using a command line utility called grpcui, to install it run below on macOS:
brew install grpcui
Once installed, we can spin up the web app to inspect our gRPC APIs using below:
grpcui -plaintext localhost:8980
When you initially run this, You will see an error message like below, Oh.. no… 🤦
Failed to compute set of methods to expose: server does not support the reflection API
What went wrong here?
The message is pretty self-explanatory.
To play around with APIs with GRPC Web UI you need to enable server reflection support, If curious, read this tutorial to enable this for the server and understand this in detail
But in short, we need to add package import below and add ProtoReflectionService to our serverBuilder in examples/src/main/java/io/grpc/examples/routeguide/RouteGuideServer.java
import io.grpc.protobuf.services.ProtoReflectionService;
public RouteGuideServer(ServerBuilder<?> serverBuilder, int port, Collection<Feature> features) {
this.port = port;
server =
serverBuilder
.addService(new RouteGuideService(features))
.addService(ProtoReflectionService.newInstance())
.build();
}
After making this change, we can again build and run the server as above and start the grpcui
./gradlew installDist -PskipAndroid=true
./build/install/examples/bin/route-guide-server
# Run this in a different tab that where server is running
grpcui -plaintext localhost:8980
We can see that the gRPC UI is up and running. We can see our 4 service methods being available
We can select the method that we want to test and then select a request payload and call that API directly
Let’s say we want to call GetFeature method with a test latitude and longitude
We can enter a test latitude and longitude in Request Form
Or directly update the request JSON in Raw Request
For example, say we want to check any feature at given lat or long
{
"latitude": -8,
"longitude": 115
}
If there is no feature at the specified location then the API returns an empty name, like below:
{
"name": "",
"location": {
"latitude": -8,
"longitude": 115
}
}
Congratulations! 🎀 You’ve made your first gRPC API request.
In the follow-up post, we will understand other methods for this API and also understand what a unit test for these API methods looks like
Thanks for the time you spent reading this 🙌. If you found this post helpful, please share it with your friends and follow me (@automationhacks) for more such insights in Software Testing and Automation. Until next time, Happy Testing 🕵🏻 and Learning! 🌱 | Newsletter | YouTube | Blog | Twitter. |
Resources
- Follow Quick Start Java gRPC to understand the components involved
- Read Introduction to gRPC and then the core concepts
- Read Overview Protocol Buffers Documentation to understand protocol buffers syntax
- Follow Basics tutorial Java gRPC to understand how to use protocol buffers to define a gRPC service
Comments