pact_consumer/lib.rs
1//! The `pact_consumer` crate provides tools for writing consumer [Pact
2//! tests][pact]. It implements the [V3 Pact specification][spec]. You can also
3//! use it as a simple HTTP mocking library for Rust.
4//!
5//! [pact]: https://docs.pact.io/ [spec]:
6//! https://github.com/pact-foundation/pact-specification
7//!
8//! ## What is Pact?
9//!
10//! [Pact][pact] is a [cross-language standard][spec] for testing the
11//! communication between the consumer of a REST API, and the code that provides
12//! that API. Test cases are written from the consumer's perspective, and they
13//! can then be exported testing the provider.
14//!
15//! The big advantages of Pact are:
16//!
17//! 1. The mocks you write to test the client can also be reused to verify that
18//! the server would actually respond the way the client expects. This gives
19//! the end-to-end assurance of integration tests (well, almost), but with
20//! the speed and convenience of unit tests.
21//! 2. Pact has been implemented in many popular languages, so you can test
22//! clients and servers in multiple languages.
23//!
24//! Whenever possible, we try to use vocabulary similar to the Ruby or
25//! JavaScript API for basic concepts, and we try to provide the same behavior.
26//! But we offer many handy builder methods to make tests cleaner.
27//!
28//! ## How to use it
29//!
30//! To use this crate, add it to your `[dev-dependencies]` in your `Cargo.toml`:
31//!
32//! ```toml
33//! [dev-dependencies]
34//! pact_consumer = "~1.4.0"
35//! ```
36//!
37//! Once this is done, you can then write the following inside a function marked
38//! with `#[tokio::test]`:
39//!
40//! ```no_run
41//! use pact_consumer::prelude::*;
42//!
43//! // Define the Pact for the test, specify the names of the consuming
44//! // application and the provider application.
45//! let provider_service = PactBuilder::new("Consumer", "Alice Service")
46//! // Start a new interaction. We can add as many interactions as we want.
47//! .interaction("a retrieve Mallory request", "", |mut i| {
48//! // Defines a provider state. It is optional.
49//! i.given("there is some good mallory");
50//! // Define the request, a GET (default) request to '/mallory'.
51//! i.request.path("/mallory");
52//! // Define the response we want returned. We assume a 200 OK
53//! // response by default.
54//! i.response
55//! .content_type("text/plain")
56//! .body("That is some good Mallory.");
57//! // Return the interaction builder back to the pact framework
58//! i
59//! })
60//! .start_mock_server(None, None);
61//! ```
62//!
63//! You can than use an HTTP client like `reqwest` to make requests against your
64//! server.
65//!
66//! ```rust
67//! # tokio_test::block_on(async {
68//! # use pact_models::pact::Pact;
69//! # use std::io::Read;
70//! # use pact_consumer::prelude::*;
71//! # let provider_service = PactBuilder::new("Consumer", "Alice Service")
72//! # // Start a new interaction. We can add as many interactions as we want.
73//! # .interaction("a retrieve Mallory request", "", |mut i| {
74//! # // Defines a provider state. It is optional.
75//! # i.given("there is some good mallory");
76//! # // Define the request, a GET (default) request to '/mallory'.
77//! # i.request.path("/mallory");
78//! # // Define the response we want returned. We assume a 200 OK
79//! # // response by default.
80//! # i.response
81//! # .content_type("text/plain")
82//! # .body("That is some good Mallory.");
83//! # // Return the interaction builder back to the pact framework
84//! # i
85//! # }).start_mock_server(None, None);
86//!
87//! // You would use your actual client code here.
88//! let mallory_url = provider_service.path("/mallory");
89//! let mut response = reqwest::get(mallory_url).await.expect("could not fetch URL")
90//! .text().await.expect("Could not read response body");
91//! assert_eq!(response, "That is some good Mallory.");
92//!
93//! // When `provider_service` goes out of scope, your pact will be validated,
94//! // and the test will fail if the mock server didn't receive matching
95//! // requests.
96//! # });
97//! ```
98//!
99//! ## Matching using patterns
100//!
101//! You can also use patterns like `like!`, `each_like!` or `term!` to allow
102//! more general matches, and you can build complex patterns using the
103//! `json_pattern!` macro:
104//!
105//! ```
106//! use pact_consumer::prelude::*;
107//!
108//! PactBuilder::new("quotes client", "quotes service")
109//! .interaction("add a new quote to the database", "", |mut i| {
110//! i.request
111//! .post()
112//! .path("/quotes")
113//! .json_utf8()
114//! .json_body(json_pattern!({
115//! // Allow the client to send any string as a quote.
116//! // When testing the server, use "Eureka!".
117//! "quote": like!("Eureka!"),
118//! // Allow the client to send any string as an author.
119//! // When testing the server, use "Archimedes".
120//! "by": like!("Archimedes"),
121//! // Allow the client to send an array of strings.
122//! // When testing the server, send a single-item array
123//! // containing the string "greek".
124//! "tags": each_like!("greek"),
125//! }));
126//!
127//! i.response
128//! .created()
129//! // Return a location of "/quotes/12" to the client. When
130//! // testing the server, allow it to return any numeric ID.
131//! .header("Location", term!("^/quotes/[0-9]+$", "/quotes/12"));
132//! i
133//! });
134//! ```
135//!
136//! The key insight here is this "pact" can be used to test both the client and
137//! the server:
138//!
139//! - When testing the **client**, we allow the request to be anything which
140//! matches the patterns—so `"quote"` can be any string, not just `"Eureka!"`.
141//! But we respond with the specified values, such as `"/quotes/12"`.
142//! - When testing the **server**, we send the specified values, such as
143//! `"Eureka!"`. But we allow the server to respond with anything matching the
144//! regular expression `^/quotes/[0-9]+$`, because we don't know what database
145//! ID it will use.
146//!
147//! Also, when testing the server, we may need to set up particular database
148//! fixtures. This can be done using the string passed to `given` in the
149//! examples above.
150//!
151//! ## Testing using domain objects
152//!
153//! Normally, it's best to generate your JSON using your actual domain objects.
154//! This is easier, and it reduces duplication in your code.
155//!
156//! ```
157//! use pact_consumer::prelude::*;
158//! use serde::{Deserialize, Serialize};
159//!
160//! /// Our application's domain object representing a user.
161//! #[derive(Deserialize, Serialize)]
162//! struct User {
163//! /// All users have this field.
164//! name: String,
165//!
166//! /// The server may omit this field when sending JSON, or it may send it
167//! /// as `null`.
168//! comment: Option<String>,
169//! }
170//!
171//! // Create our example user using our normal application objects.
172//! let example = User {
173//! name: "J. Smith".to_owned(),
174//! comment: None,
175//! };
176//!
177//! PactBuilder::new("consumer", "provider")
178//! .interaction("get all users", "", |mut i| {
179//! i.given("a list of users in the database");
180//! i.request.path("/users");
181//! i.response
182//! .json_utf8()
183//! .json_body(each_like!(
184//! // Here, `strip_null_fields` will remove `comment` from
185//! // the generated JSON, allowing our pattern to match
186//! // missing comments, null comments, and comments with
187//! // strings.
188//! strip_null_fields(serde_json::json!(example)),
189//! ));
190//! i
191//! })
192//! .build();
193//! ```
194//!
195//! ## Testing messages
196//!
197//! Testing message consumers is supported. There are two types: asynchronous messages and synchronous request/response.
198//!
199//! ### Asynchronous messages
200//!
201//! Asynchronous messages are you normal type of single shot or fire and forget type messages. They are typically sent to a
202//! message queue or topic as a notification or event. With Pact tests, we will be testing that our consumer of the messages
203//! works with the messages setup as the expectations in test. This should be the message handler code that processes the
204//! actual messages that come off the message queue in production.
205//!
206//! The generated Pact file from the test run can then be used to verify whatever created the messages adheres to the Pact
207//! file.
208//!
209//! ```rust
210//! use pact_consumer::prelude::*;
211//! use expectest::prelude::*;
212//! use serde_json::{Value, from_slice};
213//!
214//! // Define the Pact for the test (you can setup multiple interactions by chaining the given or message_interaction calls)
215//! // For messages we need to use the V4 Pact format.
216//! let mut pact_builder = PactBuilder::new_v4("message-consumer", "message-provider"); // Define the message consumer and provider by name
217//! pact_builder
218//! // Adds an interaction given the message description and type.
219//! .message_interaction("Mallory Message", |mut i| {
220//! // defines a provider state. It is optional.
221//! i.given("there is some good mallory".to_string());
222//! // Can set the test name (optional)
223//! i.test_name("a_message_consumer_side_of_a_pact_goes_a_little_something_like_this");
224//! // Set the contents of the message. Here we use a JSON pattern, so that matching rules are applied
225//! i.json_body(json_pattern!({
226//! "mallory": like!("That is some good Mallory.")
227//! }));
228//! // Need to return the mutated interaction builder
229//! i
230//! });
231//!
232//! // This will return each message configured with the Pact builder. We need to process them
233//! // with out message handler (it should be the one used to actually process your messages).
234//! for message in pact_builder.messages() {
235//! let bytes = message.contents.contents.value().unwrap();
236//!
237//! // Process the message here as it would if it came off the queue
238//! let message: Value = serde_json::from_slice(&bytes).unwrap();
239//!
240//! // Make some assertions on the processed value
241//! expect!(message.as_object().unwrap().get("mallory")).to(be_some().value("That is some good Mallory."));
242//! }
243//! ```
244//!
245//! ### Synchronous request/response messages
246//!
247//! Synchronous request/response messages are a form of message interchange were a request message is sent to another service and
248//! one or more response messages are returned. Examples of this would be things like Websockets and gRPC.
249//!
250//! ```rust
251//! # use bytes::Bytes;
252//! # struct MessageHandler {}
253//! # struct MockProvider { pub message: Bytes }
254//! # impl MessageHandler { fn process(bytes: Bytes, provider: &MockProvider) -> anyhow::Result<&str> { Ok("That is some good Mallory.") } }
255//! use pact_consumer::prelude::*;
256//! use pact_consumer::*;
257//! use expectest::prelude::*;
258//! use serde_json::{Value, from_slice};
259//!
260//! // Define the Pact for the test (you can setup multiple interactions by chaining the given or message_interaction calls)
261//! // For synchronous messages we also need to use the V4 Pact format.
262//! let mut pact_builder = PactBuilder::new_v4("message-consumer", "message-provider"); // Define the message consumer and provider by name
263//! pact_builder
264//! // Adds an interaction given the message description and type.
265//! .synchronous_message_interaction("Mallory Message", |mut i| {
266//! // defines a provider state. It is optional.
267//! i.given("there is some good mallory".to_string());
268//! // Can set the test name (optional)
269//! i.test_name("a_synchronous_message_consumer_side_of_a_pact_goes_a_little_something_like_this");
270//! // Set the contents of the request message. Here we use a JSON pattern, so that matching rules are applied.
271//! // This is the request message that is going to be forwarded to the provider
272//! i.request_json_body(json_pattern!({
273//! "requestFor": like!("Some good Mallory, please.")
274//! }));
275//! // Add a response message we expect the provider to return. You can call this multiple times to add multiple messages.
276//! i.response_json_body(json_pattern!({
277//! "mallory": like!("That is some good Mallory.")
278//! }));
279//! // Need to return the mutated interaction builder
280//! i
281//! });
282//!
283//! // For our test we want to invoke our message handling code that is going to initialise the request
284//! // to the provider with the request message. But we need some mechanism to mock the response
285//! // with the resulting response message so we can confirm our message handler works with it.
286//! for message in pact_builder.synchronous_messages() {
287//! // the request message we must make
288//! let request_message_bytes = message.request.contents.value().unwrap();
289//! // the response message we expect to receive from the provider
290//! let response_message_bytes = message.response.first().unwrap().contents.value().unwrap();
291//!
292//! // We use a mock here, assuming there is a Trait that controls the response message that our
293//! // mock can implement.
294//! let mock_provider = MockProvider { message: response_message_bytes };
295//! // Invoke our message handler to send the request message from the Pact interaction and then
296//! // wait for the response message. In this case it will be the response via the mock provider.
297//! let response = MessageHandler::process(request_message_bytes, &mock_provider);
298//!
299//! // Make some assertions on the processed value
300//! expect!(response).to(be_ok().value("That is some good Mallory."));
301//! }
302//! ```
303//!
304//! ## Using Pact plugins
305//!
306//! The consumer test builders support using Pact plugins. Plugins are defined in the [Pact plugins project](https://github.com/pact-foundation/pact-plugins).
307//! To use plugins requires the use of Pact specification V4 Pacts.
308//!
309//! To use a plugin, first you need to let the builder know to load the plugin and then configure the interaction based on
310//! the requirements for the plugin. Each plugin may have different requirements, so you will have to consult the plugin
311//! docs on what is required. The plugins will be loaded from the plugin directory. By default, this is `~/.pact/plugins` or
312//! the value of the `PACT_PLUGIN_DIR` environment variable.
313//!
314//! There are generic functions that take JSON data structures and pass these on to the plugin to
315//! setup the interaction. For request/response HTTP interactions, there is the `contents` function on the request and
316//! response builders. For message interactions, the function is called `contents_from`.
317//!
318//! For example, if we use the CSV plugin from the plugins project, our test would look like:
319//!
320//! ```no_run
321//! use expectest::prelude::*;
322//! use regex::Regex;
323//! use pact_consumer::prelude::*;
324//! #[tokio::test]
325//! async fn test_csv_client() {
326//! // Create a new V4 Pact
327//! let csv_service = PactBuilder::new_v4("CsvClient", "CsvServer")
328//! // Tell the builder we are using the CSV plugin
329//! .using_plugin("csv", None).await
330//! // Add the interaction for the CSV request
331//! .interaction("request for a CSV report", "core/interaction/http", |mut i| async move {
332//! // Path to the request we are going to make
333//! i.request.path("/reports/report001.csv");
334//! // Response we expect back
335//! i.response
336//! .ok()
337//! // We use the generic "contents" function to send the expected response data to the plugin in JSON format
338//! .contents(ContentType::from("text/csv"), json!({
339//! "csvHeaders": false,
340//! "column:1": "matching(type,'Name')",
341//! "column:2": "matching(number,100)",
342//! "column:3": "matching(datetime, 'yyyy-MM-dd','2000-01-01')"
343//! })).await;
344//! i.clone()
345//! })
346//! .await
347//! // Now start the mock server
348//! .start_mock_server_async(None, None)
349//! .await;
350//!
351//! // Now we can make our actual request for the CSV file and validate the response
352//! let client = CsvClient::new(csv_service.url().clone());
353//! let data = client.fetch("report001.csv").await.unwrap();
354//!
355//! let columns: Vec<&str> = data.trim().split(",").collect();
356//! expect!(columns.get(0)).to(be_some().value(&"Name"));
357//! expect!(columns.get(1)).to(be_some().value(&"100"));
358//! let date = columns.get(2).unwrap();
359//! let re = Regex::new("\\d{4}-\\d{2}-\\d{2}").unwrap();
360//! expect!(re.is_match(date)).to(be_true());
361//! }
362//! ```
363//!
364//! ## More Info
365//!
366//! For more advice on writing good pacts, see [Best Practices][].
367//!
368//! [Best Practices]: https://docs.pact.io/best_practices/consumer.html
369#![warn(missing_docs)]
370#[doc = include_str!("../README.md")]
371
372// Child modules which define macros (must be first because macros are resolved)
373// in source inclusion order).
374#[macro_use]
375pub mod patterns;
376#[cfg(test)]
377#[macro_use]
378mod test_support;
379
380// Other child modules.
381pub mod builders;
382pub mod mock_server;
383pub mod util;
384
385/// A "prelude" or a default list of import types to include. This includes
386/// the basic DSL, but it avoids including rarely-used types.
387///
388/// ```
389/// use pact_consumer::prelude::*;
390/// ```
391pub mod prelude {
392 pub use crate::{
393 like,
394 each_like,
395 each_like_helper,
396 term,
397 json_pattern,
398 json_pattern_internal
399 };
400 pub use crate::builders::{HttpPartBuilder, PactBuilder, PactBuilderAsync};
401 #[cfg(feature = "plugins")] pub use crate::builders::plugin_builder::PluginInteractionBuilder;
402 pub use crate::mock_server::{StartMockServer, ValidatingMockServer};
403 pub use crate::patterns::{
404 EachLike,
405 Like,
406 Term,
407 ObjectMatching,
408 EachKey,
409 EachValue,
410 JsonPattern,
411 Pattern,
412 StringPattern,
413 each_key,
414 each_value
415 };
416 #[cfg(feature = "datetime")] pub use crate::patterns::{DateTime};
417 pub use crate::util::strip_null_fields;
418 pub use pact_mock_server::mock_server::MockServerConfig;
419}
420
421/// Consumer version
422pub const PACT_CONSUMER_VERSION: Option<&'static str> = option_env!("CARGO_PKG_VERSION");