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)

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.

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))

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))

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
  }
)

Edit this page on GitHub