gRPC Checks
Checks specific to the Gatling gRPC protocol
Checks
The following components of a gRPC request can be checked, each having their own built-in:
- Status code and description
- Header(s)
- Trailer(s)
- The returning response(s) or message(s)
Status
A gRPC status is composed of two parts:
- The status code, defined by the enumeration
io.grpc.Status.Code
- A description optional
statusCode
Targets the status code itself:
statusCode().is(Status.Code.OK)
statusCode().shouldBe(Status.Code.OK)
statusCode.is(Status.Code.OK)
Status is the only check that has defaults in the gRPC protocol.
If you don’t define an explicit status code check on gRPC requests or gRPC protocol, Gatling will perform an implicit
check that will verify that the response status code is Status.Code.OK
.
statusDescription
Targets the description part of a status:
statusDescription().is("actual status description")
statusDescription().shouldBe("actual status description")
statusDescription.is("actual status description")
The description being optional, its absence can be tested using isNull
:
statusDescription().isNull()
statusDescription().isNull()
statusDescription.isNull
statusCause
Targets the cause of a status. Note that since the cause is a Throwable
, you cannot easily compare it for equality;
but you can, for instance, check a specific field (like in this example):
statusCause().transform(Throwable::getMessage).is("actual cause message")
statusCause().transform { it.message }.shouldBe("actual cause message")
statusCause.transform(_.getMessage).is("actual cause message")
The cause being optional, its absence can be tested using isNull
:
statusCause().isNull()
statusCause().isNull()
statusCause.isNull
Headers
With gRPC Java, headers are defined as metadata, i.e., a key-value pair.
header
To check a header value, it is required to use a Metadata.Key
as defined in the io.grpc
package. Here using the
the header name and the default ASCII marshaller:
header(
Metadata.Key.of("header", Metadata.ASCII_STRING_MARSHALLER)
).is("value")
header(
Metadata.Key.of("header", Metadata.ASCII_STRING_MARSHALLER)
).shouldBe("value")
header(
Metadata.Key.of("header", Metadata.ASCII_STRING_MARSHALLER)
).is("value")
A header can be multivalued and checked against a collection:
header(
Metadata.Key.of("header", Metadata.ASCII_STRING_MARSHALLER)
).findAll().is(Arrays.asList("value one", "value two"))
header(
Metadata.Key.of("header", Metadata.ASCII_STRING_MARSHALLER)
).findAll().shouldBe(listOf("value one", "value two"))
header(
Metadata.Key.of("header", Metadata.ASCII_STRING_MARSHALLER)
).findAll.is(List("value one", "value two"))
Shortcuts exists to help the usage of ASCII/binary headers that uses the default marshallers.
asciiHeader
Shortcut for a header with the default ASCII marshaller, i.e. io.grpc.Metadata#ASCII_STRING_MARSHALLER
:
asciiHeader("header").is("value")
asciiHeader("header").shouldBe("value")
asciiHeader("header").is("value")
binaryHeader
And here with the default binary marshaller, i.e. io.grpc.Metadata#BINARY_BYTE_MARSHALLER
:
binaryHeader("header-bin").is("value".getBytes(UTF_8))
binaryHeader("header-bin").shouldBe("value".toByteArray(UTF_8))
binaryHeader("header-bin").is("value".getBytes(UTF_8))
-bin
.Trailers
trailer
To check a trailer value, it is required to use a Metadata.Key
as defined in the io.grpc
package. Here using the
trailer name and the default ASCII marshaller:
trailer(
Metadata.Key.of("trailer", Metadata.ASCII_STRING_MARSHALLER)
).is("value")
trailer(
Metadata.Key.of("trailer", Metadata.ASCII_STRING_MARSHALLER)
).shouldBe("value")
trailer(
Metadata.Key.of("trailer", Metadata.ASCII_STRING_MARSHALLER)
).is("value")
A trailer can be multivalued and checked against a collection:
trailer(
Metadata.Key.of("trailer", Metadata.ASCII_STRING_MARSHALLER)
).findAll().is(Arrays.asList("value one", "value two"))
trailer(
Metadata.Key.of("trailer", Metadata.ASCII_STRING_MARSHALLER)
).findAll().shouldBe(listOf("value one", "value two"))
trailer(
Metadata.Key.of("trailer", Metadata.ASCII_STRING_MARSHALLER)
).findAll.is(List("value one", "value two"))
Shortcuts exists to help the usage of ASCII/binary trailers that uses the default marshallers.
asciiTrailer
Shortcut for a trailer with the default ASCII marshaller, i.e. io.grpc.Metadata#ASCII_STRING_MARSHALLER
:
asciiTrailer("header").is("value")
asciiTrailer("header").shouldBe("value")
asciiTrailer("header").is("value")
binaryTrailer
And here with the default binary marshaller:
binaryTrailer("trailer-bin").is("value".getBytes(UTF_8))
binaryTrailer("trailer-bin").shouldBe("value".toByteArray(UTF_8))
binaryTrailer("trailer-bin").is("value".getBytes(UTF_8))
-bin
.Messages
response
Targets the message part.
response(ResponseMessage::getMessage)
.is("actual result")
response(ResponseMessage::getMessage)
.shouldBe("actual result")
response((result: ResponseMessage) => result.message)
.is("actual result")
Note that the lambda’s parameter type cannot be inferred by the compiler and must be specified explicitly.
Priorities
Checks are performed in the following order independently of the order in which they are defined:
- Status
- Headers
- Trailers
- Response (Message)
In the following example, even though the status check is defined last, it will be performed first:
grpc("unary checks")
.unary(ExampleServiceGrpc.getExampleMethod())
.send(message)
.check(
response(ResponseMessage::getMessage).is("message value"),
asciiTrailer("trailer").is("trailer value"),
asciiHeader("header").is("header value"),
statusCode().is(Status.Code.OK)
);
grpc("unary checks")
.unary(ExampleServiceGrpc.getExampleMethod())
.send(message)
.check(
response(ResponseMessage::getMessage).shouldBe("message value"),
asciiTrailer("trailer").shouldBe("trailer value"),
asciiHeader("header").shouldBe("header value"),
statusCode().shouldBe(Status.Code.OK)
)
grpc("unary checks")
.unary(ExampleServiceGrpc.METHOD_EXAMPLE)
.send(message)
.check(
response((result: ResponseMessage) => result.message).is("message value"),
asciiTrailer("trailer").is("trailer value"),
asciiHeader("header").is("header value"),
statusCode.is(Status.Code.OK)
)
If you don’t define a status check yourself, the default status code check will be applied first.
Scope by gRPC method kind
Unary
For unary calls, checks are defined after the send
method:
grpc("unary")
.unary(ExampleServiceGrpc.getExampleMethod())
.send(message)
.check(
response(ResponseMessage::getMessage).is("message value")
);
grpc("unary")
.unary(ExampleServiceGrpc.getExampleMethod())
.send(message)
.check(
response(ResponseMessage::getMessage).shouldBe("message value")
)
grpc("unary")
.unary(ExampleServiceGrpc.METHOD_EXAMPLE)
.send(message)
.check(
response((result: ResponseMessage) => result.message).is("message value")
)
Streams
For all stream types, checks are defined at the same time as the stream: before the stream is started and/or before message(s) are sent.
Status, headers and trailers checks are applied only once per stream. Message checks are applied every time a message is received.
With a server stream:
GrpcServerStreamingServiceBuilder<RequestMessage, ResponseMessage> serverStream =
grpc("server stream")
.serverStream(ExampleServiceGrpc.getExampleMethod())
.check(
response(ResponseMessage::getMessage).is("message value")
);
exec(
serverStream.send(message),
serverStream.awaitStreamEnd()
);
val serverStream =
grpc("server stream")
.serverStream(ExampleServiceGrpc.getExampleMethod())
.check(
response(ResponseMessage::getMessage).shouldBe("message value")
)
exec(
serverStream.send(message),
serverStream.awaitStreamEnd()
)
val serverStream =
grpc("server stream")
.serverStream(ExampleServiceGrpc.METHOD_EXAMPLE)
.check(
response((result: ResponseMessage) => result.message).is("message value")
)
exec(
serverStream.send(message),
serverStream.awaitStreamEnd
)
A client stream:
GrpcClientStreamingServiceBuilder<RequestMessage, ResponseMessage> clientStream =
grpc("client stream")
.clientStream(ExampleServiceGrpc.getExampleMethod())
.check(
response(ResponseMessage::getMessage).is("message value")
);
exec(
clientStream.start(),
clientStream.send(message),
clientStream.halfClose(),
clientStream.awaitStreamEnd()
);
val clientStream =
grpc("client stream")
.clientStream(ExampleServiceGrpc.getExampleMethod())
.check(
response(ResponseMessage::getMessage).shouldBe("message value")
)
exec(
clientStream.start(),
clientStream.send(message),
clientStream.halfClose(),
clientStream.awaitStreamEnd()
)
val clientStream =
grpc("client stream")
.clientStream(ExampleServiceGrpc.METHOD_EXAMPLE)
.check(
response((result: ResponseMessage) => result.message).is("message value")
)
exec(
clientStream.start,
clientStream.send(message),
clientStream.halfClose,
clientStream.awaitStreamEnd
)
And a bidi stream:
GrpcBidirectionalStreamingServiceBuilder<RequestMessage, ResponseMessage> bidiStream =
grpc("bidi stream")
.bidiStream(ExampleServiceGrpc.getExampleMethod())
.check(
response(ResponseMessage::getMessage).is("message value")
);
exec(
bidiStream.start(),
bidiStream.send(message),
bidiStream.halfClose(),
bidiStream.awaitStreamEnd()
);
val bidiStream =
grpc("bidi stream")
.bidiStream(ExampleServiceGrpc.getExampleMethod())
.check(
response(ResponseMessage::getMessage).shouldBe("message value")
)
exec(
bidiStream.start(),
bidiStream.send(message),
bidiStream.halfClose(),
bidiStream.awaitStreamEnd()
)
val bidiStream =
grpc("bidi stream")
.bidiStream(ExampleServiceGrpc.METHOD_EXAMPLE)
.check(
response((result: ResponseMessage) => result.message).is("message value")
)
exec(
bidiStream.start,
bidiStream.send(message),
bidiStream.halfClose,
bidiStream.awaitStreamEnd
)
Limitations to the gRPC checks API
It is not currently possible to apply different checks to specific incoming messages in the same stream. Be wary that
saveAs
will overwrite previously saved values:
GrpcServerStreamingServiceBuilder<RequestMessage, ResponseMessage> serverStream =
grpc("server stream")
.serverStream(ExampleServiceGrpc.getExampleMethod())
.check(
// Overwrites the 'result' key for each message received
response(ResponseMessage::getMessage).saveAs("result")
);
exec(
serverStream.send(message),
serverStream.awaitStreamEnd((main, forked) ->
// Message checks operate on a forked session, we need
// to reconcile it with the main session at the end
main.set("result", forked.getString("result"))
),
exec(session -> {
// 'result' contains the last message received
var result = session.getString("result");
return session;
})
);
val serverStream =
grpc("server stream")
.serverStream(ExampleServiceGrpc.getExampleMethod())
.check(
// Overwrites the 'result' key for each message received
response(ResponseMessage::getMessage).saveAs("result")
)
exec(
serverStream.send(message),
serverStream.awaitStreamEnd { main, forked ->
// Message checks operate on a forked session, we need
// to reconcile it with the main session at the end
main.set("result", forked.getString("result"))
},
exec { session ->
// 'result' contains the last message received
val result = session.getString("result")
session
}
)
val serverStream =
grpc("server stream")
.serverStream(ExampleServiceGrpc.METHOD_EXAMPLE)
.check(
// Overwrites the 'result' key for each message received
response((result: ResponseMessage) => result.message).saveAs("result")
)
exec(
serverStream.send(message),
serverStream.awaitStreamEnd { (main, forked) =>
// Message checks operate on a forked session, we need
// to reconcile it with the main session at the end
main.set("result", forked("result").as[String])
},
exec { session =>
// 'result' contains the last message received
val result = session("result").as[String]
session
}
)