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 文件的完整流程:

消费者契约生成流程 1. 定义消费者期望 方法 + 路径 + 状态 2. 设置交互 请求 + 响应匹配器 3. 执行测试 Verify() 验证 4. 生成Pact文件 consumer-provider.json 5. 共享契约 Git / Pact Broker 关键点 只定义实际使用的字段 使用类型匹配器提高灵活性

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 等复杂交互。这有助于快速建立信心并排查问题。