Photo by Nick Fewings on Unsplash
What is gRPC?
gRPC - is a modern Remote Procedure Call framework created and popularized by Google but now is under CNCF license.
The main idea of the framework is to provide for clients, written in various languages, a possibility to execute remote calls to the server as if it is a local call.
Picture from grpc.io docs
The core concept of gRPC is that all communication between server and client is based on Protocol Buffers defined in proto files. The server will implement and support this interface, and the client will generate and use stub code.
More about gRPC and concepts - here.
Introducing: Greeter service
gRPC service example is taken from the official akka-grpc guide.
server-grpc-example repository contains a proto file with a service definition and its implementation. You can execute it on a local machine by running:
$ sbt runMain io.grpc.examples.helloworld.GreeterClient
By default, the server will start at localhost:8080.
Protobuf definition for the service is the following:
syntax = "proto3";
import "google/protobuf/timestamp.proto";
option java_multiple_files = true;
option java_package = "example.myapp.example.myapp.helloworld.grpc";
option java_outer_classname = "HelloWorldProto";
service GreeterService {
rpc SayHello(HelloRequest) returns (HelloReply) {}
rpc ItKeepsTalking(stream HelloRequest) returns (HelloReply) {}
rpc ItKeepsReplying(HelloRequest) returns (stream HelloReply) {}
rpc StreamHellos(stream HelloRequest) returns (stream HelloReply) {}
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
google.protobuf.Timestamp timestamp = 2;
}
As you can see, Greeter Service has four methods:
- SayHello() method for unary calls: a client sends a single request to the server and gets a single response back
- ItKeepsTalking() method for client streaming: the client will send a bunch of messages to the server and, after that, will wait for a single reply back
- ItKeepsReplying() method for server streaming: the client will read a stream of messages from a server until there are no more new messages.
- StreamHellos() method for bi-directional communication: the client and the server send a stream of messages to each other.
Automate by consuming proto files
Test automation process for gRPC based service consists of the following steps:
-
Add sbt plugin for Akka gRPC - create plugins.sbt file with the following content in the repo/project folder.
addSbtPlugin("com.lightbend.akka.grpc" % "sbt-akka-grpc" % "2.0.0")
-
Add dependencies to build.sbt and update the project.
enablePlugins(AkkaGrpcPlugin) libraryDependencies += "org.scalatest" %% "scalatest" % "3.3.0-SNAP2" % Test
-
Get all proto files from the server repository. In our case, it will be just copying hello_world.proto file from server repo and pasting it to src/main/protobuf directory.
As an improvement, proto files can(and should!) be automatically downloaded from the server or central proto file repository to all client repositories. It prevents the client from synchronization errors as the development of the server functionality evolves.
-
Generate gRPC clients from proto files. As our build system in sbt, you need to execute the command
$ sbt compile
in Terminal or choose it from sbt command list in IDE.
As a result, all clients generated from proto file can be found at /target/scala-2.13/akka-grpc/main folder. -
Add Matchers and ScalaFutures traits to test class definition.
class GreeterServiceApiTest extends AnyFlatSpec with Matchers with ScalaFutures {
-
Configure default waiting timeout for responses.
implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = Span(3, Minutes), interval = Span(10, Millis))
-
Add configuration for gRPC client.
val host = "127.0.0.1" val port = 8080 implicit protected val system: ActorSystem = ActorSystem("api-test") val clientSettings: GrpcClientSettings = GrpcClientSettings.connectToServiceAt(host, port).withTls(false) val client: GreeterService = GreeterServiceClient(clientSettings)
-
Implement the tests
"Greeter Service" should "handle unary requests and response" in { val name = getRandomValue val response = client.sayHello(HelloRequest(name)).futureValue response.getTimestamp should not be null response.message should be(s"Hello, $name") } "Greeter Service" should "handle client streaming" in { val data = getRandomList(3) val response = client.itKeepsTalking(Source(data.map(HelloRequest(_)))).futureValue response.timestamp should not be null response.message should be(s"Hello, ${data.mkString(", ")}") } "Greeter Service" should "handle server streaming" in { val name = getRandomValue val responses = client.itKeepsReplying(HelloRequest(name)).runWith(Sink.seq).futureValue responses should not be empty val messages = responses.map(_.message).toList all(messages) should not be "" } "Greeter Service" should "handle bi-directional streaming" in { val data = getRandomList(3) val responses = client.streamHellos(Source(data.map(HelloRequest(_)))).runWith(Sink.seq).futureValue responses should not be empty val messages = responses.map(_.message).toList all(messages) should not be "" }
Pay attention that we can't get a response straight away - we need to wait until Future completes and then get the data by executing .futureValue.
Matchers library provides a fluent api for writing assertions - for single values and collections of elements.
-
Execute the tests either from IDE or from Terminal
$ sbt test
Conclusions
Automation of gRPC-based services is a little bit similar to automating SOAP-based services. In both cases, you have some utilities for generating clients as a code (ws-consume for SOAP and akka-grpc or scalapb for gRPC).
After you generate a client - an overall approach is the same as for any API tests - firing a request with test data and asserting responses.
gRPC brings a bit more complexity with client, server, and bi-directional streaming - but you can handle it without any problems using futures and collecting all responses in Lists.
As always, all code is available in github-repo
Top comments (0)