Rust契约测试库pact_matching的使用,实现消费者驱动契约测试与API模拟验证

Rust契约测试库pact_matching的使用,实现消费者驱动契约测试与API模拟验证

使用方式

在Cargo.toml中添加依赖:

[dependencies]
pact_matching = "2.0"

注意:这个crate使用2024 Rust版本,需要Rust 1.85.0或更高版本。

核心功能

该crate提供了三个主要函数:

  • match_request - 匹配HTTP请求
  • match_response - 匹配HTTP响应
  • match_message - 匹配消息

这些函数接受来自pact_models crate的预期和实际请求、响应或消息模型,并返回不匹配项的向量。

完整示例

下面是一个完整的消费者驱动契约测试示例:

use pact_matching::models::{Pact, Request, Response};
use pact_matching::match_request;
use std::collections::HashMap;

#[test]
fn test_consumer_contract() {
    // 1. 创建消费者契约
    let mut pact = Pact::default();
    
    // 2. 定义预期请求
    let expected_request = Request {
        method: "GET".to_string(),
        path: "/api/user/123".to_string(),
        headers: Some({
            let mut headers = HashMap::new();
            headers.insert("Accept".to_string(), "application/json".to_string());
            headers
        }),
        query: None,
        body: None,
        ..Default::default()
    };
    
    // 3. 定义预期响应
    let expected_response = Response {
        status: 200,
        headers: Some({
            let mut headers = HashMap::new();
            headers.insert("Content-Type".to_string(), "application/json".to_string());
            headers
        }),
        body: Some(serde_json::json!({
            "id": "123",
            "name": "John Doe",
            "email": "john.doe@example.com
        }).to_string()),
        ..Default::default()
    };
    
    // 4. 将交互添加到契约
    pact.interactions.push(Box::new(expected_request.clone(), expected_response.clone()));
    
    // 5. 模拟实际请求
    let actual_request = Request {
        method: "GET".to_string(),
        path: "/api/user/123".to_string(),
        headers: Some({
            let mut headers = HashMap::new();
            headers.insert("Accept".to_string(), "application/json".to_string());
            headers
        }),
        query: None,
        body: None,
        ..Default::default()
    };
    
    // 6. 进行请求匹配
    let mismatches = match_request(&expected_request, &actual_request);
    
    // 7. 验证没有不匹配项
    assert!(mismatches.is_empty(), "Found mismatches: {:?}", mismatches);
    
    // 8. 模拟API提供者测试
    let actual_response = Response {
        status: 200,
        headers: Some({
            let mut headers = HashMap::new();
            headers.insert("Content-Type".to_string(), "application/json".to_string());
            headers
        }),
        body: Some(serde_json::json!({
            "id": "123",
            "name": "John Doe",
            "email": "john.doe@example.com"
        }).to_string()),
        ..Default::default()
    };
    
    // 9. 进行响应匹配
    let mismatches = match_response(&expected_response, &actual_response);
    
    // 10. 验证没有不匹配项
    assert!(mismatches.is_empty(), "Found mismatches: {:?}", mismatches);
}

匹配规则

Pact支持通过契约文件中的matchingRules元素扩展每种对象(请求或响应)的匹配规则。这是一组JSON路径字符串到匹配器的映射。当比较项目时,如果有匹配规则条目对应项目的路径,比较将委托给定义的匹配器。

支持的匹配器

匹配器 示例配置 描述
正则表达式 { "match": "regex", "regex": "\\d+" } 对值的字符串表示执行正则表达式匹配
类型 { "match": "type" } 执行基于类型的匹配,即如果它们是相同类型则相等
包含 { "match": "include", "value": "substr" } 检查值的字符串表示是否包含子字符串
整数 { "match": "integer" } 检查值的类型是否为整数
日期时间 { "match": "datetime", "format": "yyyy-MM-dd HH:ss:mm" } 根据日期时间格式匹配值的字符串表示

读取和写入Pact文件

pact_models crate中的Pact结构体有读取和写入pact JSON文件的方法。它支持所有规范版本直到V4,但会将V1、V1.1和V2规范文件转换为V3格式。

特性

所有特性默认启用:

  • datetime: 启用日期和时间表达式和生成器的支持
  • xml: 启用对XML文档解析的支持
  • plugins: 启用使用插件
  • multipart: 启用对MIME多部分体的支持

这个库实现了匹配HTTP请求和响应所需的核心匹配逻辑,基于V4 pact规范。


1 回复

Rust契约测试库pact_matching的使用:实现消费者驱动契约测试与API模拟验证

介绍

pact_matching是Rust实现的Pact契约测试库,用于支持消费者驱动契约测试(Consumer-Driven Contract Testing)。它允许消费者和提供者服务之间定义明确的契约,确保API交互的兼容性。

完整示例demo

消费者端完整示例

use pact_consumer::prelude::*;
use serde_json::json;

// 假设我们有一个用户服务客户端
struct UserServiceClient {
    base_url: String
}

impl UserServiceClient {
    fn new(base_url: String) -> Self {
        Self { base_url }
    }

    fn get_user(&self, id: u32) -> Result<User, reqwest::Error> {
        let client = reqwest::blocking::Client::new();
        let response = client
            .get(&format!("{}/users/{}", self.base_url, id))
            .header("Accept", "application/json")
            .send()?
            .json()?;
        Ok(response)
    }
}

#[derive(Debug, serde::Deserialize)]
struct User {
    id: u32,
    name: String,
    email: String
}

#[test]
fn test_user_service_client() {
    // 1. 定义Pact契约
    let pact = PactBuilder::new("UserServiceConsumer", "UserService")
        .interaction("获取用户信息", "", |mut i| {
            i.given("用户ID 123存在");
            i.request.path("/users/123");
            i.request.method("GET");
            i.response
                .status(200)
                .header("Content-Type", "application/json")
                .json_body(json!({
                    "id": Like(123),
                    "name": "John Doe",
                    "email": Regex("^[\\w\\.]+@[\\w]+\\.[a-z]{2,}$".to_string(), "john@example.com")
                }));
            i
        })
        .build();

    // 2. 启动模拟服务
    let mock_server = pact.start_mock_server();
    
    // 3. 创建客户端并测试
    let client = UserServiceClient::new(mock_server.url());
    let user = client.get_user(123).unwrap();
    
    // 4. 验证响应数据
    assert_eq!(user.id, 123);
    assert_eq!(user.name, "John Doe");
    
    // 5. 验证交互是否按预期发生
    mock_server.matched();
}

提供者端完整示例

use pact_matching::models::Pact;
use std::fs::File;
use reqwest::blocking::Client;
use serde_json::Value;

// 提供者服务实现
fn handle_get_user(id: u32) -> (u16, Value, Vec<(String, String)>) {
    // 这里应该是实际的业务逻辑
    if id == 123 {
        let body = json!({
            "id": 123,
            "name": "John Doe",
            "email": "john@example.com"
        });
        let headers = vec![
            ("Content-Type".to_string(), "application/json".to_string())
        ];
        (200, body, headers)
    } else {
        (404, json!({"error": "User not found"}), vec![])
    }
}

#[test]
fn test_provider_verification() {
    // 1. 加载契约文件
    let file = File::open("pacts/userserviceconsumer-userservice.json").unwrap();
    let pact = Pact::from_json("pacts/userserviceconsumer-userservice.json", &file).unwrap();
    
    // 2. 对每个交互进行验证
    for interaction in pact.interactions {
        // 3. 准备请求参数
        let path = interaction.request.path;
        let method = interaction.request.method;
        let headers: Vec<_> = interaction.request.headers.iter()
            .map(|(k, v)| (k.clone(), v.clone()))
            .collect();
        
        // 4. 提取ID参数
        let id = path.split('/').last().unwrap().parse::<u32>().unwrap();
        
        // 5. 调用实际服务处理
        let (status, body, actual_headers) = handle_get_user(id);
        
        // 6. 验证状态码
        assert_eq!(status, interaction.response.status);
        
        // 7. 验证响应头
        for (header_name, expected_value) in &interaction.response.headers {
            let actual_value = actual_headers.iter()
                .find(|(name, _)| name == header_name)
                .map(|(_, v)| v)
                .unwrap();
            assert_eq!(actual_value, expected_value);
        }
        
        // 8. 验证响应体
        if let Some(expected_body) = interaction.response.body {
            let expected_json: Value = serde_json::from_str(&expected_body).unwrap();
            assert_eq!(body, expected_json);
        }
    }
}

使用说明

  1. 消费者端测试会生成契约文件在target/pacts/目录下
  2. 提供者端测试需要这些契约文件来验证实现
  3. 可以使用Pact Broker来管理和共享契约

高级匹配器示例

use pact_consumer::prelude::*;

#[test]
fn test_advanced_matching() {
    let pact = PactBuilder::new("OrderConsumer", "OrderService")
        .interaction("创建订单", "", |mut i| {
            i.given("用户123有足够余额");
            i.request
                .path("/orders")
                .method("POST")
                .header("X-Request-ID", Regex("[a-f0-9]{32}".to_string(), "abc123"))
                .json_body(json_pattern!({
                    "userId": Like(123),
                    "items": EachLike(json_pattern!({
                        "productId": Integer(1..100),
                        "quantity": Integer(1..=10),
                        "price": Decimal(10.0)
                    }), min=1),
                    "total": Decimal(10.0)
                }));
            
            i.response
                .status(201)
                .json_body(json_pattern!({
                    "orderId": Regex("[A-Z0-9]{8}".to_string(), "ORDER123"),
                    "status": "CREATED",
                    "createdAt": Regex("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z".to_string(), "2023-01-01T00:00:00Z")
                }));
            i
        })
        .build();
    
    let mock_server = pact.start_mock_server();
    // ... 测试代码
}
回到顶部