Karate-gRPC adds first-class support for testing gRPC. The challenge of sending and consuming messages in async fashion is solved via an elegant API.
- Unified syntax similar to HTTP but focused on gRPC
- Mix HTTP and gRPC calls within the same test flow
- Support for all gRPC modes: Unary, Server Streaming, Client Streaming and Bidirectional
- Set up multiple async connections if needed
- Support for parallel execution
- Support for performance testing
- Express data as JSON and leverage Karate's powerful assertions
- Use your
*.proto
files directly, no code-generation required - Automatic Protobuf to JSON conversion
- Support for SSL/TLS and using certificates for secure auth
To run Karate tests using this library, you need a license from Karate labs. You can email [email protected] and request a license.
To develop and run feature files from the IDE you need to upgrade to the paid versions of Karate Labs official plugins for IntelliJ or VS Code.
You need a Maven or Gradle project. Please use the latest available version. The dependency info can be found here: https://central.sonatype.com/artifact/io.karatelabs/karate-grpc
The karate.lic
file you receive should be placed in a .karate
folder in your project root. You can also change the default path where the license is expected - by setting a KARATE_LICENSE_PATH
environment property.
You can find a sample project here: Karate gRPC Example.
Async handling requires a little more complexity than simple API tests, but karate-grpc
still keeps it simple. Here is an example:
* def session = karate.consume('grpc')
* session.host = 'localhost'
* session.port = 9555
* session.proto = 'classpath:karate/hello.proto'
* session.service = 'HelloService'
* session.method = 'Hello'
* session.send({ name: 'John' })
* match session.pop() == { message: 'hello John' }
Note how the syntax is future-proof, and support for other async protocols such as kafka
and websocket
is very similar.
Typically you name the returned variable from karate.consume()
as session. Now you can set properties before calling session.start()
or session.send()
.
Note how all the connection and service / method parameters can be set on the session before starting to send and receive messages.
Note that variables defined using def
or in karate-config.js
will work just like you expect in Karate. The syntax leans more towards "pure JS" than Karate's HTTP keywords.
This means that within JSON, variables will work directly without needing to use the
'#(variableName)'
syntax for embedded expressions that you may be used to.
The properties that you can set on the object returned by karate.consume()
are as follows:
Name | Description |
---|---|
host | Host name |
port | Port number (string variable references also work for convenience) |
proto | Relative / Prefixed path to *.proto file |
protoRoots | (optional) array of relative / prefixed paths which are the search "roots" for dependencies or proto files which are "imports" |
service | gRPC service name |
method | gRPC method name |
count | (defaults to 1) the number of response messages to wait for before the collect() or pop() method returns, see example |
filter | (optional) A JS function, see example |
stream | (defaults to false ) Enable client-side streaming, so you have to call flush() after send() |
trustCert | (optional) Trusted authority certificate, see TLS |
clientCert | (optional) Client certificate for "mutual auth", see TLS |
clientKey | (optional) Client key for "mutual auth", see TLS |
For convenience, it is possible to set multiple properties on the session in one line.
So instead of:
* def session = karate.consume('grpc')
* session.host = 'localhost'
* session.port = 9555
* session.proto = 'classpath:karate/hello.proto'
You can do:
* def session = karate.consume('grpc')
* session.config = { host: 'localhost', port: 9555, proto: 'classpath:karate/hello.proto' }
And session.config
can be called multiple times within a test flow, and you can over-write existing properties. This can be convenient for long flows.
For actions such as sending messages or "collecting" async responses, you call methods on the session object. Since these are JS method invocations, they use round brackets and may take method arguments.
Once you have a session and have set connection parameters, you can start sending messages.
* session.send({ name: 'John' })
* match session.collect() == [{ message: 'hello John' }]
Although there is technically a session.start()
method behind the scenes, it is automatically called behind the scenes for the first message sent, for convenience.
Refer to the example for a working demo.
Conversations are easy, you can continue to send()
and collect()
as long as needed:
* session.send({ name: 'John' })
* match session.pop() == { message: 'hello John' }
* session.send({ name: 'Smith' })
* match session.pop() == { message: 'hello Smith' }
As seen above, collect()
is designed to return an array of messages, since we are dealing with async channels. But since in many cases, you would be running with session.count = 1
there is a convenient pop()
method that returns the first message in the "buffer".
The example above can be re-written as follows since session.count = 1
:
* session.send({ name: 'John' })
* match session.pop() == { message: 'hello John' }
Since this defaults to 1
the first message received will stop the "wait mode" and "un-block" any call to collect()
or pop()
after a send()
.
For example, here is how to tell the test to wait for 3 messages:
* session.count = 3
* session.send({ name: 'John' })
* def result = session.collect()
* match result ==
"""
[
{ message: 'hello John 1' },
{ message: 'hello John 2' },
{ message: 'hello John 3' }
]
"""
This is set up by doing session.stream = true
as shown below.
The default is false
, but when you want to simulate multiple messages sent by the client asynchonously - you can set session.stream = true
.
This also means that you should call flush()
if you have to signal that the client is "done" or "complete". This is needed only in cases where the server waits for the client to "complete" before responding.
Here is an example of client-side streaming, as explained in the sections above.
* session.stream = true
* session.send({ name: 'John' })
* session.send({ name: 'Smith' })
* session.send({ name: 'Jane' })
* session.flush()
* match session.pop() == { message: 'hello [John, Smith, Jane]' }
Here above, the server happened to be programmed to send a single message when the client has signalled that it is "complete".
Refer to the example for a working demo.
Bi-directional streaming is simple, combining all the concepts introduced above, mainly session.stream
and session.count
.
Here we ask the test to wait until 3 messages were received from the server. Meanwhile we can send multiple messages.
* session.stream = true
* session.count = 3
* session.send({ name: 'John' })
* session.send({ name: 'Smith' })
* session.send({ name: 'Jane' })
* match session.collect() ==
"""
[
{ message: 'hello [John]' },
{ message: 'hello [John, Smith]' },
{ message: 'hello [John, Smith, Jane]' }
]
"""
Refer to the example for a working demo.
Optional way to filter for only some kinds of messages to collect.
You can use JS functions and be very dynamic. For example:
* session.filter = x => x.message != 'zero'
If the server only accepts encrypted connections, you can configure a trusted certificate store (CA) as follows:
* session.trustCert = 'classpath:ca.crt'
You can also present a client certificate and key as follows. Here we are also demonstrating the use of session.config
for reducing the lines of code.
* session.config = { trustCert: 'classpath:ca.crt', clientCert: 'classpath:client.crt', clientKey: 'classpath:client.pem' }
The Karate gRPC example includes working samples of TLS and TLS with "mutual auth".
The steps taken to create the (self-signed) certificates for the CA, server and client for the demo are explained in this file for your reference: crypto.txt.