Pact plugin for testing messages and gRPC service calls encoded with as Protocol buffers using the Pact contract testing framework.
This plugin provides support for matching and verifying Protobuf messages and gRPC service calls. It fits into the Pact contract testing framework and extends Pact testing for Protocol buffer payloads.
- Requirements to use it
- Installation
- Supported features
- Unsupported features
- Using the plugin
- Support
- Contributing to the plugin
- Development Roadmap
This plugin provides matching and verification of Protobuf proto3 encoded messages to the Pact contract testing framework. It requires a version of the Pact framework that supports the V4 Pact specification as well as the Pact plugin framework.
Supported Pact versions:
To support compiling Protocol Buffer proto files requires a version of the Protocol Buffer compiler.
The executable binaries and plugin manifest file for the plugin can be downloaded from the project releases page. There will be an executable for each operating system and architecture. If your particular operating system or architecture is not supported, please send a request to [email protected] with the details.
To install the plugin requires the plugin executable binary as well as the plugin manifest file to be unpacked/copied into
a Pact plugin directory. By default, this will be .pact/plugins/protobuf-<version>
in the home directory (i.e. $HOME/.pact/plugins/protobuf-0.0.0
).
Example installation of Linux version 0.0.0:
- Create the plugin directory if needed:
mkdir -p ~/.pact/plugins/protobuf-0.0.0
- Download the plugin manifest into the directory:
wget https://github.com/pactflow/pact-protobuf-plugin/releases/download/v-0.0.0/pact-plugin.json -O ~/.pact/plugins/protobuf-0.0.0/pact-plugin.json
- Download the plugin executable into the directory:
wget https://github.com/pactflow/pact-protobuf-plugin/releases/download/v-0.0.0/pact-protobuf-plugin-linux-x86_64.gz -O ~/.pact/plugins/protobuf-0.0.0/pact-protobuf-plugin-linux-x86_64.gz
- Unpack the plugin executable:
gunzip -N ~/.pact/plugins/protobuf-0.0.0/pact-protobuf-plugin-linux-x86_64.gz
Note: The unpacked executable name must match the entryPoint
value in the manifest file. By default this is
pact-protobuf-plugin
on unix* and pact-protobuf-plugin.exe
on Windows.
The default plugin directory ($HOME/.pact/plugins
) can be changed by setting the PACT_PLUGIN_DIR
environment variable.
The plugin can automatically download the correct version of the Protocol buffer compiler for the current operating system and architecture. By default, it will download the compiler from https://github.com/protocolbuffers/protobuf/releases and then unpack it into the plugin's installation directory.
The plugin executes the following steps:
- Look for a valid
protoc/bin/protoc
in the plugin installation directory - If not found, look for a
protoc-{version}-{OS}.zip
in the plugin installation directory and unpack that (i.e. for Linux it will look forprotoc-3.19.1-linux-x86_64.zip
). - If not found, try download protoc using the
downloadUrl
entry in the plugin manifest file - Otherwise, fallback to using the system installed protoc
If the plugin is going to run in an environment that does not allow automatic downloading of files, then you can do any of the following:
- Download the protoc archive and place it in the plugin installation directory. It will need to be the correct version and operating system/architecture.
- Download the protoc archive and unpack it into the plugin installation directory. It will need to be in a
protoc
directory. Do this if the current version is not supported for your operating system/architecture. - Change the
downloadUrl
entry in the plugin manifest to point to a location that the file can be downloaded from. - Install the correct version of the protoc compiler as an operating system package. It must then be on the executable path when the plugin runs. For instance, for Alpine Linux this will need to be done as the downloaded versions will not work.
The plugin will log to both standard output and a file (plugin.log) in the plugin log file. Common Rust library entries
for debug and trace levels will be filtered out. The log level will be set by the LOG_LEVEL
environment variable that
is passed into the plugin process (this should be set by the framework calling it).
The logging can be configured using a YAML file (log-config.yaml) in the plugin installation directory. For documentation on the file format, see Log4rs.
An example file which enables trace level logs for the plugin:
appenders:
stdout:
kind: console
file:
kind: file
path: "plugin.log"
encoder:
pattern: "{d(%Y-%m-%dT%H:%M:%S%Z)} {l} [{T}] {t} - {m}{n}"
root:
level: warn # This will be replaced at runtime with the current logging level
appenders:
- stdout
- file
# set some of the common Rust libraries to info level to reduce noise at debug/trace
loggers:
h2:
level: info
hyper:
level: info
tracing:
level: warn
tokio:
level: info
tokio_util:
level: info
mio:
level: info
The plugin currently supports proto3 formatted messages and service calls.
It supports the following:
- Scalar fields (Double, Float, Int64, Uint64, Int32, Uint32, Fixed64, Fixed32, Bool, Sfixed32, Sfixed64, Sint32, Sint64).
- Variable length fields (String, Bytes).
- Enum fields.
- Embedded messages.
- Map fields (with a string key).
- Repeated fields.
- RPC Service method calls (requires mocking of gRPC methods calls as gRPC is not currently supported).
The following features are currently unsupported, but will be supported in a later release:
- oneOf fields.
- Map fields with scalar keys.
- Map fields with enum keys.
- default values for fields.
- packed fields.
- required fields.
- gRPC service calls (gRPC mock server).
- Testing/verifying options.
The following features will not be supported by this plugin:
- proto2
- Groups
The following features may be supported in a future release, but are not currently planned to be supported:
- Map fields where the key is not a string or scalar value.
- gRPC streaming (either oneway or bidirectional).
This plugin will register itself with the Pact framework for the application/protobuf
content type.
Using this plugin, you can write Pact tests that verify either a single Protobuf message (i.e. a message provider sends a single, or one-shot, message to a consumer), or you can verify a service method call where there is an input message and an output message.
Single message tests are supported by using the V4 asynchronous message Pact format, and the service method calls use the V4 synchronous message Pact format.
For an overview how asynchronous messages work with Pact, see Non-HTTP testing (Message Pact).
In this scenario, a message provider writes a Protocol Buffer message to some one-way transport mechanism, like a message queue, and a consumer then reads it. With this style of testing, the transport mechanism is abstracted away.
The message consumer test is written using the Pact Message test DSL. The test DSL defines the expected message format, and then the consumer is tested with an example message generated by the test framework.
For an example of a message consumer test:
The message provider is verified by getting it to generate a message, and then this is verified against the Pact file from the consumer. There are two main ways of verifying the provider:
- Write a test in the provider code base that can call the provider to generate the message.
- Use an HTTP proxy server that can call the provider and return the generated message, and then use a Pact framework verifier to verify it.
For an example of the latter form, see Simple Example Protobuf provider.
NOTE: gRPC service calls are not currently supported directly, but will be supported in a future version.
With a service method call, the consumer creates an input message, then invokes a service method and gets an output message as the response. The most common service call is via the gRPC RPC framework.
To test the service message consumer, we write a Pact test that defines the expected input (or request) message and the expected output (or response message). The Pact test framework will generate an example input and output message.
To execute the test, we need to intercept the service method call and verify that the message the consumer generated was correct, then we return the output message and verify that the consumer processed it correctly. This can be achieved using a test mocking library.
For an example:
The Protocol Buffer service providers normally extend an interface generated by the protoc compiler. To test them, we need a mechanism to get the Pact verifier to pass in the input message from the Pact file and then get the output message from the service and compare that to the output message from the Pact file.
We can use the same mechanism as for message pact (see Non-HTTP testing (Message Pact)), were we create an HTTP proxy server to receive the input message from the verifier and invoke the service method implementation to get the output message.
There are two main ways to run the verification:
- Execute the Pact verifier, providing the source of the Pact file, and configure it to use the HTTP mock server.
- Write a test in the provider's code base. For an example of doing this in Rust, see a test that verifies this plugin.
The consumer tests need to get the plugin loaded and configure the expected messages to use in the test. This is done
using the usingPlugin
(or using_plugin
, depending on the language implementation) followed by the content for the test
in some type of map form.
For each field of the message that we want in the contract, we define an entry with the field name as the key and a matching definition as the value. For documentation on the matching definition format, see Matching Rule definition expressions.
For example, for a JVM test (taken from Protocol Buffer Java examples) we would use the PactBuilder class:
// this example taken from https://developers.google.com/protocol-buffers/docs/javatutorial#defining-your-protocol-format
message Person {
string name = 1;
int32 id = 2;
string email = 3;
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phones = 4;
}
builder
// Tell the Pact framework to load the protobuf plugin
.usingPlugin("protobuf")
// Define the expected message (description) and the type of interaction. Here is is an asynchronous message.
.expectsToReceive("Person Message", "core/interaction/message")
// Provide the data for the test
.with(Map.of(
// For a single asynchronous message, we just provide the contents for the message. For RPC service calls, there
// will be a request and response message
"message.contents", Map.of(
// set the content type, so the Pact framework will know to send it to the Protobuf plugin
"pact:content-type", "application/protobuf",
// pact:proto contains the source proto file, which is required to be able to test the interaction
"pact:proto", filePath("addressbook.proto"),
// provide the name of the message type we are going to test (defined in the proto file)
"pact:message-type", "Person",
// We can then setup the expected fields of the message
"name", "notEmpty('Fred')", // The name field must not be empty, and we use Fred in our tests
"id", "matching(regex, '100\\d+', '1000001')", // The id field must match the regular expression, and we use 1000001 in the tests
"email", "matching(regex, '\\w+@[a-z0-9\\.]+', '[email protected]')" // Emails must match a regular expression
// phones is a repeated field, so we define an example that all values must match against
"phones", Map.of(
"number", "matching(regex, '(\\+\\d+)?(\\d+\\-)?\\d+\\-\\d+', '+61-03-1234-5678')" // Phone numbers must match a regular expression
// We don't include type, as it is an emum and has a default value, so it is optional
// but we could have done something like matching(equalTo, 'WORK')
)
)
))
Join us on slack in the #protobufs channel
or
Twitter: @pact_up
Stack Overflow: stackoverflow.com/questions/tagged/pact
PRs are always welcome!
For details on the V4 Pact specification, refer to https://github.com/pact-foundation/pact-specification/tree/version-4
For details on the Pact plugin framework, refer to https://github.com/pact-foundation/pact-plugins
Before raising an issue, make sure you have checked the open and closed issues to see if an answer is provided there. There may also be an answer to your question on stackoverflow.
Please provide the following information with your issue to enable us to respond as quickly as possible.
- The relevant versions of the packages you are using (plugin and Pact versions).
- The steps to recreate your issue.
- An executable code example where possible.
- Fork it
- Create your feature branch (git checkout -b my-new-feature)
- Commit your changes (git commit -am 'feat: Add some feature')
- Push to the branch (git push origin my-new-feature)
- Create new Pull Request
We follow the Conventional Changelog message conventions. Please ensure you follow the guidelines.
To build the plugin, you need a working Rust environment (version 1.58+). Refer to the Rust Guide.
The build tool used is cargo
and you can build the plugin by running cargo build
. This will compile the plugin and
put the generated files in target/debug
. The main plugin executable is pact-protobuf-plugin
and this will need to be copied into the Pact plugin directory. See the installation instructions above.
You can run all the unit tests by executing cargo test --lib
.
There is a Pact test that verifies the plugin aqainst the Pact file published to pact-foundation.pactflow.io.
Running this test requires a Pactflow API token and the plugin to be built and installed. See the installation instructions above.
The test is run using cargo test --test pact_verify
.
Pact plugin development board: https://github.com/pact-foundation/pact-plugins/projects/1
This plugin is released under the MIT License and is copyright © 2021-22 Pactflow.
The Pactflow logos are copyright © Pactflow and may not be used without permission.