Files
cs-note/hhs/MQ/10-监控与运维/41-MQ-测试策略.md
T

190 lines
6.2 KiB
Markdown
Raw Normal View History

2026-05-24 20:51:06 +08:00
---
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-部署]]