#pact #cdc #test-framework

pact_consumer

Pact-Rust 模块,提供编写消费者 pact 测试的支持

64 个版本 (14 个稳定版)

1.3.0 2024年7月29日
1.2.2 2024年6月14日
1.1.2 2024年2月12日
1.1.0 2023年12月20日
0.2.0 2016年10月9日

测试 中排名 26

Download history 812/week @ 2024-04-26 1078/week @ 2024-05-03 1203/week @ 2024-05-10 1133/week @ 2024-05-17 1084/week @ 2024-05-24 1476/week @ 2024-05-31 1246/week @ 2024-06-07 1698/week @ 2024-06-14 1649/week @ 2024-06-21 1355/week @ 2024-06-28 1637/week @ 2024-07-05 1709/week @ 2024-07-12 1302/week @ 2024-07-19 2247/week @ 2024-07-26 1416/week @ 2024-08-02 1423/week @ 2024-08-09

每月下载量 6,737
用于 2 crates

MIT 和 Apache-2.0 许可

1.5MB
38K SLoC

Rust pact 测试 DSL,用于编写消费者 pact 测试

此库提供了一个用于在 Rust 中编写消费者 pact 测试的测试 DSL。它支持 V3 pact 规范V4 pact 规范

在线 Rust 文档

使用方法

要使用它,将其添加到您的 cargo 清单中的 dev-dependencies

[dev-dependencies]
pact_consumer = "1.3"

现在,您可以使用消费者 DSL 编写 pact 测试。

use pact_consumer::prelude::*;

#[tokio::test]
async fn a_service_consumer_side_of_a_pact_goes_a_little_something_like_this() {
   let alice_service = PactBuilder::new("Consumer", "Alice Service")
       // Start a new interaction. We can add as many interactions as we want.
       .interaction("a retrieve Mallory request", "", |mut i| {
           // Defines a provider state. It is optional.
           i.given("there is some good mallory");
           // Define the request, a GET (default) request to '/mallory'.
           i.request.path("/mallory");
           // Define the response we want returned. We assume a 200 OK
           // response by default.
           i.response
               .content_type("text/plain")
               .body("That is some good Mallory.");
           // Return the interaction builder back to the pact framework
           i
       }).start_mock_server(None);
  
   // You would use your actual client code here.
   let mallory_url = alice_service.path("/mallory");
   let mut response = reqwest::get(mallory_url).await.expect("could not fetch URL")
     .text().await.expect("Could not read response body");
   assert_eq!(response, "That is some good Mallory.");
  
   // When `alice_service` goes out of scope, your pact will be validated,
   // and the test will fail if the mock server didn't receive matching
   // requests.
}

更改输出目录

默认情况下,pact 文件将被写入 target/pacts。要更改此设置,设置环境变量 PACT_OUTPUT_DIR

强制覆盖 pact 文件

写入时,pacts 会与现有 pact 文件合并。要将此行为更改为始终覆盖文件,将环境变量 PACT_OVERWRITE 设置为 true

测试消息

支持测试消息消费者。有两种类型:异步消息和同步请求/响应。

异步消息

异步消息是您正常的单次或“发送后即忘”类型的消息。它们通常作为通知或事件发送到消息队列或主题。使用 Pact 测试,我们将测试我们的消息消费者是否与测试中设置的消息期望一起正常工作。这应该是处理从生产中的消息队列接收的实际消息的消息处理程序代码。

然后可以使用测试运行生成的 Pact 文件来验证创建消息的内容是否遵循 Pact 文件。

use pact_consumer::prelude::*;

#[test]
fn a_message_consumer_side_of_a_pact_goes_a_little_something_like_this() {

    // Define the Pact for the test (you can setup multiple interactions by chaining the given or message_interaction calls)
    // For messages we need to use the V4 Pact format.
    let pact_builder = PactBuilder::PactBuilder::new_v4("message-consumer", "message-provider"); // Define the message consumer and provider by name
    pact_builder
      // defines a provider state. It is optional.
      .given("there is some good mallory".to_string())                                           
      // Adds an interaction given the message description and type.
      .message_interaction("Mallory Message", "core/interaction/message", |mut i| { 
        // Can set the test name (optional)
        i.test_name("a_message_consumer_side_of_a_pact_goes_a_little_something_like_this");
        // Set the contents of the message. Here we use a JSON pattern, so that matching rules are applied
        i.json_body(json_pattern!({
          "mallory": like!("That is some good Mallory.")
        }));
        // Need to return the mutated interaction builder
        i
      });

    // This will return each message configured with the Pact builder. We need to process them
    // with out message handler (it should be the one used to actually process your messages).
    for message in pact_builder.messages() {
      let bytes = message.contents.contents.value().unwrap();
      
      // Process the message here as it would if it came off the queue
      let message: Value = serde_json::from_slice(&bytes);      

      // Make some assertions on the processed value
      expect!(message.as_object().unwrap().get("mallory")).to(be_some().value());
    }
}

同步请求/响应消息

同步请求/响应消息是一种消息交互方式,其中将请求消息发送到另一个服务,并返回一个或多个响应消息。例如,Websockets和gRPC就是这种类型的例子。

use pact_consumer::prelude::*;
use expectest::prelude::*;
use serde_json::{Value, from_slice};

#[test]
fn a_synchronous_message_consumer_side_of_a_pact_goes_a_little_something_like_this() {

  // Define the Pact for the test (you can setup multiple interactions by chaining the given or message_interaction calls)
  // For synchronous messages we also need to use the V4 Pact format.
  let mut pact_builder = PactBuilder::new_v4("message-consumer", "message-provider"); // Define the message consumer and provider by name
  pact_builder
    // Adds an interaction given the message description and type.
    .synchronous_message_interaction("Mallory Message", "core/interaction/synchronous-message", |mut i| {
      // defines a provider state. It is optional.
      i.given("there is some good mallory".to_string());
      // Can set the test name (optional)
      i.test_name("a_synchronous_message_consumer_side_of_a_pact_goes_a_little_something_like_this");
      // Set the contents of the request message. Here we use a JSON pattern, so that matching rules are applied.
      // This is the request message that is going to be forwarded to the provider
      i.request_json_body(json_pattern!({
          "requestFor": like!("Some good Mallory, please.")
        }));
      // Add a response message we expect the provider to return. You can call this multiple times to add multiple messages.
      i.response_json_body(json_pattern!({
          "mallory": like!("That is some good Mallory.")
        }));
      // Need to return the mutated interaction builder
      i
    });

  // For our test we want to invoke our message handling code that is going to initialise the request
  // to the provider with the request message. But we need some mechanism to mock the response
  // with the resulting response message so we can confirm our message handler works with it.
  for message in pact_builder.synchronous_messages() {
    // the request message we must make
    let request_message_bytes = message.request.contents.value().unwrap();
    // the response message we expect to receive from the provider
    let response_message_bytes = message.response.first().unwrap().contents.value().unwrap();

    // We use a mock here, assuming there is a Trait that controls the response message that our
    // mock can implement.
    let mock_provider = MockProvider { message: response_message_bytes };
    // Invoke our message handler to send the request message from the Pact interaction and then
    // wait for the response message. In this case it will be the response via the mock provider.
    let response = MessageHandler::process(request_message_bytes, &mock_provider);

    // Make some assertions on the processed value
    expect!(response).to(be_ok().value("That is some good Mallory."));
  }
}

使用Pact插件

消费者测试构建器支持使用Pact插件。插件在Pact插件项目中定义。要使用插件,需要使用Pact规范V4 Pacts。

要使用插件,首先需要让构建器知道加载插件,然后根据插件的要求配置交互。每个插件可能有不同的要求,因此您需要查阅插件文档了解所需的内容。插件将从插件目录加载。默认情况下,这是~/.pact/pluginsPACT_PLUGIN_DIR环境变量的值。

存在一些通用函数,可以接受JSON数据结构并将其传递给插件以设置交互。对于请求/响应HTTP交互,在请求和响应构建器上有contents函数。对于消息交互,函数名为contents_from

例如,如果我们使用插件项目中的CSV插件,我们的测试将看起来像

#[tokio::test]
async fn test_csv_client() {
    // Create a new V4 Pact 
    let csv_service = PactBuilder::new_v4("CsvClient", "CsvServer")
    // Tell the builder we are using the CSV plugin  
    .using_plugin("csv", None).await
    // Add the interaction for the CSV request  
    .interaction("request for a CSV report", "core/interaction/http", |mut i| async move {
        // Path to the request we are going to make
        i.request.path("/reports/report001.csv");
        // Response we expect back
        i.response
          .ok()
          // We use the generic "contents" function to send the expected response data to the plugin in JSON format 
          .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
    // Now start the mock server  
    .start_mock_server_async()
    .await;
    
    // Now we can make our actual request for the CSV file and validate the response
    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());
}

依赖项

~20–59MB
~1M SLoC