4. 编写第一个消费者契约:定义消费者期望、设置交互、生成Pact文件
消费者契约是 Pact 契约测试的起点。在微服务架构中,消费者(Consumer)定义它对某个 Provider API 的调用期望,包括请求方法、路径、请求头、请求体以及期望的响应状态码和响应体。Pact 框架会将这些期望序列化为一个 JSON 格式的 Pact 文件,作为消费者与提供者之间的“契约”。
4.1 消费者契约的核心要素
一个完整的消费者契约包含以下三个核心部分:
| 要素 | 说明 | 示例 |
|---|---|---|
| 消费者期望 | 描述消费者对 Provider 接口的调用意图,包括请求的 HTTP 方法和路径 | GET /api/users/123 |
| 交互 (Interaction) | 在期望基础上,定义完整的请求细节(Headers、Body)和预期的响应细节(Status、Headers、Body) | 请求头包含 Authorization,响应返回 200 及用户 JSON |
| Pact 文件 | 将交互序列化为 JSON 格式的契约文件,供 Provider 端验证 | consumer-provider.json |
关键原则:消费者契约只定义它实际使用的字段,而不是 Provider 返回的全部字段。这保证了契约的最小化,避免 Provider 端因无关字段变更而破坏契约。
4.2 使用 Pact Go 定义消费者期望
在 Go 中,我们使用 pact-go 库的 dsl 包来构建消费者契约。以下是一个完整的示例,演示如何定义一个查询用户信息的消费者期望。
package consumer_test
import (
"testing"
"github.com/pact-foundation/pact-go/dsl"
)
func TestConsumer_GetUser(t *testing.T) {
// 1. 创建 Pact 实例
pact := &dsl.Pact{
Consumer: "UserServiceConsumer",
Provider: "UserServiceProvider",
Host: "localhost",
}
defer pact.Teardown()
// 2. 定义交互 (Interaction)
pact.
AddInteraction().
Given("用户 ID 为 123 的用户存在"). // Provider 状态
UponReceiving("获取用户信息的请求"). // 交互描述
WithRequest(dsl.Request{
Method: "GET",
Path: dsl.String("/api/users/123"),
Headers: dsl.MapMatcher{
"Authorization": dsl.String("Bearer test-token"),
},
}).
WillRespondWith(dsl.Response{
Status: 200,
Headers: dsl.MapMatcher{
"Content-Type": dsl.String("application/json"),
},
Body: dsl.Match(&User{
ID: 123,
Name: "张三",
Role: "admin",
}),
})
// 3. 执行测试:发送请求并验证响应
err := pact.Verify(func() error {
// 模拟消费者调用 Provider 的代码
user, err := GetUser("http://localhost:1234", "123")
if err != nil {
return err
}
// 验证返回的用户信息
if user.ID != 123 || user.Name != "张三" {
t.Errorf("用户信息不匹配: got %+v", user)
}
return nil
})
if err != nil {
t.Fatalf("契约验证失败: %v", err)
}
}
4.3 交互 (Interaction) 的详细配置
交互是契约的核心,它定义了请求和响应的完整匹配规则。Pact 使用匹配器(Matcher)来灵活验证字段,而不是严格的相等比较。
4.3.1 请求匹配器
| 匹配器 | 用途 | 示例 |
|---|---|---|
dsl.String() |
匹配字符串类型,忽略具体值 | dsl.String("/api/users/123") |
dsl.Integer() |
匹配整数类型 | dsl.Integer() |
dsl.MapMatcher |
匹配请求头或 JSON 对象中的键值对 | dsl.MapMatcher{"Authorization": dsl.String("Bearer test-token")} |
dsl.Term() |
使用正则表达式匹配 | dsl.Term("123", `\d+`) |
4.3.2 响应匹配器
| 匹配器 | 用途 | 示例 |
|---|---|---|
dsl.Match() |
匹配结构体或 map,只验证存在的字段 | dsl.Match(&User{ID: 123, Name: "张三"}) |
dsl.Like() |
匹配类型,忽略具体值 | dsl.Like("any-string") |
dsl.EachLike() |
匹配数组中的每个元素 | dsl.EachLike(dsl.Like("item"), 1) |
最佳实践:在定义交互时,优先使用类型匹配器(如
dsl.String()、dsl.Integer())而不是具体值。这可以使契约更灵活,避免因测试数据变化而频繁更新契约文件。
4.4 生成 Pact 文件
当消费者测试通过后,Pact 会自动在当前工作目录下生成一个 JSON 格式的 Pact 文件。文件命名规则为:{consumer}-{provider}.json。
以下是根据上述测试生成的 Pact 文件示例:
{
"consumer": {
"name": "UserServiceConsumer"
},
"provider": {
"name": "UserServiceProvider"
},
"interactions": [
{
"description": "获取用户信息的请求",
"providerState": "用户 ID 为 123 的用户存在",
"request": {
"method": "GET",
"path": "/api/users/123",
"headers": {
"Authorization": "Bearer test-token"
}
},
"response": {
"status": 200,
"headers": {
"Content-Type": "application/json"
},
"body": {
"id": 123,
"name": "张三",
"role": "admin"
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "3.0.0"
}
}
}
注意:Pact 文件是消费者和提供者之间的唯一契约。它应该被版本控制(如 Git)并共享给 Provider 团队。不要手动修改 Pact 文件,所有变更都应通过消费者测试重新生成。
4.5 消费者契约工作流程
下图展示了从定义消费者期望到生成 Pact 文件的完整流程:
4.6 常见问题与避坑指南
- 匹配器使用不当:使用
dsl.String("张三")会严格匹配字符串“张三”,而dsl.Like("张三")只匹配字符串类型。在契约中应优先使用dsl.Like()或dsl.Term()来避免硬编码。 - 忽略 Provider 状态:
Given()中的 Provider 状态是契约的重要组成部分,它告诉 Provider 端在测试前需要准备的数据条件。不要省略这个步骤。 - Pact 文件未版本控制:Pact 文件是团队间的契约,必须纳入版本控制。建议将 Pact 文件存放在独立的
pacts/目录下,并在 CI/CD 中自动上传到 Pact Broker。 - 测试与契约分离:消费者测试应该只验证契约,而不是测试 Provider 的业务逻辑。不要在
Verify()回调中编写复杂的业务断言。
实战建议:在编写第一个消费者契约时,先从最简单的 GET 请求开始,只包含一个交互。验证 Pact 文件生成成功后,再逐步添加 POST、PUT 等复杂交互。这有助于快速建立信心并排查问题。