Skip to content

Latest commit

 

History

History
246 lines (168 loc) · 10.4 KB

README.md

File metadata and controls

246 lines (168 loc) · 10.4 KB

Karate-gRPC

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.

Highlights

  • 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

License

Runtime License:

To run Karate tests using this library, you need a license from Karate labs. You can email [email protected] and request a license.

Developer 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.

Setup

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.

Sample Project

You can find a sample project here: Karate gRPC Example.

Syntax

karate.consume()

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.

Variables

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.

Config

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

session.config

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.

Methods

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.

session.send()

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

session.collect()

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".

session.pop()

The example above can be re-written as follows since session.count = 1:

    * session.send({ name: 'John' })
    * match session.pop() == { message: 'hello John' }

session.count

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' }
      ]
      """

Client Side Streaming

This is set up by doing session.stream = true as shown below.

session.stream

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.

session.flush()

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

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.

session.filter

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'

TLS

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.