Files
cs-note/hhs/MQ/10-监控与运维/41-MQ-测试策略.md
T
2026-05-24 20:51:06 +08:00

190 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-部署]]