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-部署]]
|