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);
}
}
}
使用说明
- 消费者端测试会生成契约文件在
target/pacts/
目录下 - 提供者端测试需要这些契约文件来验证实现
- 可以使用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();
// ... 测试代码
}