Golang中如何进行消费者驱动的契约测试

Golang中如何进行消费者驱动的契约测试 大家好

我编写了一个基于REST的微服务,并希望对其进行测试。

我曾在网上读到,对于微服务测试,推荐使用消费者驱动契约测试(CDCT)方法。

如何在不使用任何框架的情况下,用Go语言编写CDCT

谢谢

3 回复

关于单元测试HTTP服务器,我推荐这个视频作为入门指南:https://youtu.be/hVFEV-ieeew

更多关于Golang中如何进行消费者驱动的契约测试的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


消费者驱动的契约测试仅意味着您服务的API,即“契约”,是由客户端而非服务器定义的。这不是技术问题,而是组织问题。

您可以使用 httptest.Server 来测试任何用Go实现的REST服务器。

在Go中实现消费者驱动的契约测试(CDCT)可以通过标准库和自定义代码完成。以下是一个不使用外部框架的示例方案:

1. 定义契约结构

// contract.go
package cdc

type Contract struct {
    Consumer   string                 `json:"consumer"`
    Provider   string                 `json:"provider"`
    Request    RequestDefinition      `json:"request"`
    Response   ResponseDefinition     `json:"response"`
}

type RequestDefinition struct {
    Method   string            `json:"method"`
    Path     string            `json:"path"`
    Headers  map[string]string `json:"headers"`
    Body     interface{}       `json:"body"`
}

type ResponseDefinition struct {
    StatusCode int                    `json:"statusCode"`
    Headers    map[string]string      `json:"headers"`
    Body       map[string]interface{} `json:"body"`
}

2. 消费者端生成契约

// consumer_test.go
package consumer

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "testing"
    "yourmodule/cdc"
)

func TestUserServiceContract(t *testing.T) {
    // 模拟消费者调用
    resp, err := http.Get("http://localhost:8080/users/123")
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    body, _ := ioutil.ReadAll(resp.Body)
    var responseBody map[string]interface{}
    json.Unmarshal(body, &responseBody)

    // 生成契约
    contract := cdc.Contract{
        Consumer: "OrderService",
        Provider: "UserService",
        Request: cdc.RequestDefinition{
            Method: "GET",
            Path:   "/users/{id}",
        },
        Response: cdc.ResponseDefinition{
            StatusCode: 200,
            Body: map[string]interface{}{
                "id":   "123",
                "name": "John Doe",
                "type": "premium",
            },
        },
    }

    // 保存契约文件
    contractJSON, _ := json.MarshalIndent(contract, "", "  ")
    ioutil.WriteFile("contracts/user_service.json", contractJSON, 0644)
}

3. 提供者端验证契约

// provider_test.go
package provider

import (
    "encoding/json"
    "io/ioutil"
    "net/http"
    "net/http/httptest"
    "testing"
    "yourmodule/cdc"
)

func TestContractCompliance(t *testing.T) {
    // 加载契约文件
    data, err := ioutil.ReadFile("contracts/user_service.json")
    if err != nil {
        t.Fatal(err)
    }

    var contract cdc.Contract
    json.Unmarshal(data, &contract)

    // 启动测试服务器
    srv := httptest.NewServer(yourActualHandler())
    defer srv.Close()

    // 执行契约中的请求
    req, _ := http.NewRequest(
        contract.Request.Method,
        srv.URL+contract.Request.Path,
        nil,
    )
    
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    // 验证状态码
    if resp.StatusCode != contract.Response.StatusCode {
        t.Errorf("Expected status %d, got %d", 
            contract.Response.StatusCode, resp.StatusCode)
    }

    // 验证响应体结构
    var body map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&body)
    
    for key, expectedValue := range contract.Response.Body {
        if actualValue, exists := body[key]; !exists {
            t.Errorf("Missing field: %s", key)
        } else if actualValue != expectedValue {
            t.Errorf("Field %s: expected %v, got %v", 
                key, expectedValue, actualValue)
        }
    }
}

4. 契约匹配器(可选增强)

// matcher.go
package cdc

import (
    "reflect"
    "regexp"
)

type Matcher struct {
    Patterns map[string]string
}

func (m *Matcher) Match(expected, actual map[string]interface{}) bool {
    for key, expVal := range expected {
        actVal, exists := actual[key]
        if !exists {
            return false
        }
        
        if pattern, ok := m.Patterns[key]; ok {
            if strVal, ok := actVal.(string); ok {
                matched, _ := regexp.MatchString(pattern, strVal)
                if !matched {
                    return false
                }
            }
        } else if !reflect.DeepEqual(expVal, actVal) {
            return false
        }
    }
    return true
}

使用示例:

  1. 消费者运行测试生成契约文件
  2. 将契约文件提交到共享仓库
  3. 提供者定期运行契约验证测试
  4. 构建流水线中集成契约验证

这个方案提供了基本的CDCT功能,包括契约定义、生成和验证。可以通过添加字段类型验证、正则匹配、头信息验证等来扩展功能。

回到顶部