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