190 lines
6.2 KiB
Markdown
190 lines
6.2 KiB
Markdown
|
|
---
|
|||
|
|
tags:
|
|||
|
|
- MQ
|
|||
|
|
- 测试
|
|||
|
|
- 集成测试
|
|||
|
|
- 契约测试
|
|||
|
|
create time: 2026-05-24 19:52
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# MQ 测试策略
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
MQ 的测试面临异步性、消息顺序、环境依赖等独特挑战。本文从单元测试到混沌工程,逐层介绍 MQ 测试策略,重点讲解 Testcontainers 集成测试、Pact 契约测试和影子流量等实践方法。
|
|||
|
|
|
|||
|
|
## 正文
|
|||
|
|
|
|||
|
|
### MQ 测试的挑战
|
|||
|
|
|
|||
|
|
和同步的 HTTP API 相比,MQ 测试难在哪里?
|
|||
|
|
|
|||
|
|
1. **异步性**:消息发送后不立即得到结果,如何验证消息被正确处理?
|
|||
|
|
2. **消息顺序**:测试时如何保证消息的顺序和幂等性?
|
|||
|
|
3. **环境依赖**:没有真实的 Broker,很多行为无法验证(如 Partition 路由、Consumer Group Rebalance)
|
|||
|
|
4. **分布式一致性**:跨多个服务的事件流转,如何端到端验证?
|
|||
|
|
|
|||
|
|
> [!question]
|
|||
|
|
> 传统 API 测试可以用 `assert response.status == 200`。但 MQ 中,消息发出去后"成功"意味着什么?是 Broker 收到?还是被消费者处理完?
|
|||
|
|
|
|||
|
|
### 测试金字塔在 MQ 场景的应用
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph TD
|
|||
|
|
subgraph Pyramid["MQ 测试金字塔"]
|
|||
|
|
E2E["E2E 测试 - 完整集群 + 多服务"]
|
|||
|
|
Integration["集成测试 - Testcontainers 真实 Broker"]
|
|||
|
|
Contract["契约测试 - Pact 消息契约"]
|
|||
|
|
Unit["单元测试 - Mock Producer/Consumer"]
|
|||
|
|
end
|
|||
|
|
Unit -->|"快速, 大量"| Contract
|
|||
|
|
Contract -->|"契约保证兼容"| Integration
|
|||
|
|
Integration -->|"真实环境验证"| E2E
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 单元测试:Mock 接口,验证逻辑
|
|||
|
|
|
|||
|
|
单元测试的核心是将消息处理逻辑与 Broker 解耦。在 Go 中,通过接口抽象 Producer 和 Consumer:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 定义接口,方便 Mock
|
|||
|
|
type MessageProducer interface {
|
|||
|
|
Send(ctx context.Context, topic string, key, value []byte) error
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
type OrderHandler struct {
|
|||
|
|
producer MessageProducer
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 处理订单创建事件的业务逻辑
|
|||
|
|
func (h *OrderHandler) HandleOrderCreated(ctx context.Context, event OrderEvent) error {
|
|||
|
|
if event.Amount <= 0 {
|
|||
|
|
return fmt.Errorf("invalid amount: %d", event.Amount)
|
|||
|
|
}
|
|||
|
|
// 发送支付请求事件
|
|||
|
|
payment := PaymentEvent{OrderID: event.ID, Amount: event.Amount}
|
|||
|
|
data, _ := json.Marshal(payment)
|
|||
|
|
return h.producer.Send(ctx, "payment-requests", []byte(event.ID), data)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 单元测试:Mock Producer 验证业务逻辑
|
|||
|
|
func TestOrderHandler_HandleOrderCreated(t *testing.T) {
|
|||
|
|
mock := &MockProducer{} // 实现 MessageProducer 接口
|
|||
|
|
handler := &OrderHandler{producer: mock}
|
|||
|
|
|
|||
|
|
event := OrderEvent{ID: "order-1", Amount: 100}
|
|||
|
|
err := handler.HandleOrderCreated(context.Background(), event)
|
|||
|
|
|
|||
|
|
assert.NoError(t, err)
|
|||
|
|
assert.Equal(t, "payment-requests", mock.LastTopic())
|
|||
|
|
assert.Equal(t, "order-1", mock.LastKey())
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
单元测试快速且无外部依赖,但只能验证业务逻辑,无法保证消息格式与下游兼容。
|
|||
|
|
|
|||
|
|
### 集成测试:Testcontainers 启动真实 Broker
|
|||
|
|
|
|||
|
|
Mock 测试不到的问题——Partition 路由、序列化、Consumer Group 行为——需要真实 Broker。Testcontainers 在测试中启动真实的 Docker 容器:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
package main
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"testing"
|
|||
|
|
"time"
|
|||
|
|
|
|||
|
|
"github.com/IBM/sarama"
|
|||
|
|
"github.com/testcontainers/testcontainers-go"
|
|||
|
|
"github.com/testcontainers/testcontainers-go/modules/kafka"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
func TestKafkaIntegration(t *testing.T) {
|
|||
|
|
ctx := context.Background()
|
|||
|
|
|
|||
|
|
// 启动真实的 Kafka 容器
|
|||
|
|
kafkaContainer, err := kafka.Run(ctx,
|
|||
|
|
"confluentinc/confluent-local:7.5.0",
|
|||
|
|
)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatal(err)
|
|||
|
|
}
|
|||
|
|
defer kafkaContainer.Terminate(ctx)
|
|||
|
|
|
|||
|
|
// 获取 Broker 地址
|
|||
|
|
brokers, _ := kafkaContainer.Brokers(ctx)
|
|||
|
|
|
|||
|
|
// 创建 Producer 发送消息
|
|||
|
|
config := sarama.NewConfig()
|
|||
|
|
config.Producer.Return.Successes = true
|
|||
|
|
producer, _ := sarama.NewSyncProducer(brokers, config)
|
|||
|
|
defer producer.Close()
|
|||
|
|
|
|||
|
|
msg := &sarama.ProducerMessage{
|
|||
|
|
Topic: "test-topic",
|
|||
|
|
Value: sarama.StringEncoder("hello kafka"),
|
|||
|
|
}
|
|||
|
|
partition, offset, err := producer.SendMessage(msg)
|
|||
|
|
if err != nil {
|
|||
|
|
t.Fatalf("send failed: %v", err)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 创建 Consumer 消费并验证
|
|||
|
|
config2 := sarama.NewConfig()
|
|||
|
|
config2.Consumer.Offsets.Initial = sarama.OffsetOldest
|
|||
|
|
consumer, _ := sarama.NewConsumer(brokers, config2)
|
|||
|
|
defer consumer.Close()
|
|||
|
|
|
|||
|
|
pc, _ := consumer.ConsumePartition("test-topic", partition, offset)
|
|||
|
|
select {
|
|||
|
|
case msg := <-pc.Messages():
|
|||
|
|
if string(msg.Value) != "hello kafka" {
|
|||
|
|
t.Errorf("unexpected message: %s", msg.Value)
|
|||
|
|
}
|
|||
|
|
case <-time.After(10 * time.Second):
|
|||
|
|
t.Fatal("timeout waiting for message")
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
这段测试用 Testcontainers 自动拉起 Kafka 容器,发送一条消息后立即消费验证。整个过程在 CI 中无需预置环境,测试结束自动清理容器。
|
|||
|
|
|
|||
|
|
### 消息契约测试
|
|||
|
|
|
|||
|
|
在事件驱动架构中,Producer 和 Consumer 通过消息格式(Schema)解耦。但如果 Producer 改了字段名,Consumer 就会崩溃。**契约测试**解决这个问题。
|
|||
|
|
|
|||
|
|
Pact 框架的工作流程:
|
|||
|
|
1. **Consumer 端**定义期望的消息格式(契约)
|
|||
|
|
2. **Producer 端**验证自己产生的消息满足所有 Consumer 的契约
|
|||
|
|
3. 契约存储在 Pact Broker 中,CI 中自动验证
|
|||
|
|
|
|||
|
|
> [!question]
|
|||
|
|
> 消息契约测试和传统的 API 测试有什么本质区别?为什么在事件驱动架构中更重要?
|
|||
|
|
|
|||
|
|
关键区别在于:API 测试验证的是"请求-响应"的一对一关系,而消息契约验证的是"事件-消费者"的一对多关系。一个事件可能被 5 个服务消费,任何格式变更都必须向后兼容。
|
|||
|
|
|
|||
|
|
### 影子流量(Shadow Testing)
|
|||
|
|
|
|||
|
|
将生产流量复制到测试环境,验证新版本的消息处理逻辑是否正确,而不影响真实业务。
|
|||
|
|
|
|||
|
|
实现方式:
|
|||
|
|
- Producer 端使用拦截器,将消息副本发送到 Shadow Topic
|
|||
|
|
- Shadow Consumer 消费并处理,对比结果
|
|||
|
|
- 关键:Shadow 消费者的处理结果**不会写入生产数据库**
|
|||
|
|
|
|||
|
|
### 故障注入
|
|||
|
|
|
|||
|
|
Chaos Engineering 在 MQ 场景的应用:
|
|||
|
|
|
|||
|
|
- **Kill Broker**:随机杀掉一个 Broker,验证 Producer/Consumer 的自动故障转移
|
|||
|
|
- **网络分区**:模拟 Broker 之间网络隔离,验证脑裂防护
|
|||
|
|
- **消息延迟注入**:人为增加 Broker 响应延迟,验证超时和重试机制
|
|||
|
|
- **磁盘满**:模拟磁盘空间不足,验证告警和降级策略
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[40-MQ-性能调优]]
|
|||
|
|
- [[42-MQ-认证与授权]]
|
|||
|
|
- [[39-MQ-容器化与-K8s-部署]]
|