Pact test DSL for writing consumer pact tests in Rust
This library provides a test DSL for writing consumer pact tests in Rust. It supports the
V3 pact specification and
V4 pact specification.
Online rust docs
To use it
To use it, add it to your dev-dependencies in your cargo manifest:
[dev-dependencies]
pact_consumer = "0.8.5"
You can now write a pact test using the consumer DSL.
use pact_consumer::prelude::*;
use pact_consumer::*;
#[test]
fn a_service_consumer_side_of_a_pact_goes_a_little_something_like_this() {
let pact_runner = ConsumerPactBuilder::consumer("Consumer".to_string()) .has_pact_with("Alice Service".to_string()) .given("there is some good mallory".to_string()) .upon_receiving("a retrieve Mallory request".to_string()) .path(s!("/mallory")) .will_respond_with() .status(200)
.headers(hashmap!{ "Content-Type".to_string() => "text/html".to_string() })
.body(OptionalBody::Present("That is some good Mallory.".to_string()))
.build();
let result = pact_runner.run(&|url| {
let client = Client { url: url.clone(), .. Client::default() }; let result = client.fetch("/mallory"); expect!(result).to(be_ok().value("That is some good Mallory."));
Ok(())
});
expect!(result).to(be_equal_to(VerificationResult::PactVerified)); }
Changing the output directory
By default, the pact files will be written to target/pacts
. To change this, set the environment variable PACT_OUTPUT_DIR
.
Forcing pact files to be overwritten
Pacts are merged with existing pact files when written. To change this behaviour so that the files
are always overwritten, set the environment variable PACT_OVERWRITE
to true
.
Testing messages
Testing message consumers is supported. There are two types: asynchronous messages and synchronous request/response.
Asynchronous messages
Asynchronous messages are you normal type of single shot or fire and forget type messages. They are typically sent to a
message queue or topic as a notification or event. With Pact tests, we will be testing that our consumer of the messages
works with the messages setup as the expectations in test. This should be the message handler code that processes the
actual messages that come off the message queue in production.
The generated Pact file from the test run can then be used to verify whatever created the messages adheres to the Pact
file.
use pact_consumer::prelude::*;
use pact_consumer::*;
#[tokio::test]
async fn a_message_consumer_side_of_a_pact_goes_a_little_something_like_this() {
let pact_builder = PactBuilder::PactBuilder::new_v4("message-consumer", "message-provider"); pact_builder
.given("there is some good mallory".to_string())
.message_interaction("Mallory Message", "core/interaction/message", |mut i| async move {
i.test_name("a_message_consumer_side_of_a_pact_goes_a_little_something_like_this");
i.json_body(json_pattern!({
"mallory": like!("That is some good Mallory.")
}));
i
})
.await;
for message in pact_builder.messages() {
let bytes = message.contents.contents.value().unwrap();
let message: Value = serde_json::from_slice(&bytes);
expect!(message.as_object().unwrap().get("mallory")).to(be_some().value());
}
}
Synchronous request/response messages
Synchronous request/response messages are a form of message interchange were a request message is sent to another service and
one or more response messages are returned. Examples of this would be things like Websockets and gRPC.
use pact_consumer::prelude::*;
use pact_consumer::*;
use expectest::prelude::*;
use serde_json::{Value, from_slice};
#[tokio::test]
async fn a_synchronous_message_consumer_side_of_a_pact_goes_a_little_something_like_this() {
let mut pact_builder = PactBuilder::new_v4("message-consumer", "message-provider"); pact_builder
.synchronous_message_interaction("Mallory Message", "core/interaction/synchronous-message", |mut i| async move {
i.given("there is some good mallory".to_string());
i.test_name("a_synchronous_message_consumer_side_of_a_pact_goes_a_little_something_like_this");
i.request_json_body(json_pattern!({
"requestFor": like!("Some good Mallory, please.")
}));
i.response_json_body(json_pattern!({
"mallory": like!("That is some good Mallory.")
}));
i
})
.await;
for message in pact_builder.synchronous_messages() {
let request_message_bytes = message.request.contents.value().unwrap();
let response_message_bytes = message.response.first().unwrap().contents.value().unwrap();
let mock_provider = MockProvider { message: response_message_bytes };
let response = MessageHandler::process(request_message_bytes, &mock_provider);
expect!(response).to(be_ok().value("That is some good Mallory."));
}
}
Using Pact plugins
The consumer test builders support using Pact plugins. Plugins are defined in the Pact plugins project.
To use plugins requires the use of Pact specification V4 Pacts.
To use a plugin, first you need to let the builder know to load the plugin and then configure the interaction based on
the requirements for the plugin. Each plugin may have different requirements, so you will have to consult the plugin
docs on what is required. The plugins will be loaded from the plugin directory. By default, this is ~/.pact/plugins
or
the value of the PACT_PLUGIN_DIR
environment variable.
There are generic functions that take JSON data structures and pass these on to the plugin to
setup the interaction. For request/response HTTP interactions, there is the contents
function on the request and
response builders. For message interactions, the function is called contents_from
.
For example, if we use the CSV plugin from the plugins project, our test would look like:
#[tokio::test]
async fn test_csv_client() {
let csv_service = PactBuilder::new_v4("CsvClient", "CsvServer")
.using_plugin("csv", None).await
.interaction("request for a CSV report", "core/interaction/http", |mut i| async move {
i.request.path("/reports/report001.csv");
i.response
.ok()
.contents(ContentType::from("text/csv"), json!({
"csvHeaders": false,
"column:1": "matching(type,'Name')",
"column:2": "matching(number,100)",
"column:3": "matching(datetime, 'yyyy-MM-dd','2000-01-01')"
})).await;
i.clone()
})
.await
.start_mock_server_async()
.await;
let client = CsvClient::new(csv_service.url().clone());
let data = client.fetch("report001.csv").await.unwrap();
let columns: Vec<&str> = data.trim().split(",").collect();
expect!(columns.get(0)).to(be_some().value(&"Name"));
expect!(columns.get(1)).to(be_some().value(&"100"));
let date = columns.get(2).unwrap();
let re = Regex::new("\\d{4}-\\d{2}-\\d{2}").unwrap();
expect!(re.is_match(date)).to(be_true());
}