You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Go中NATS订阅测试:解决*nats.Subscription无法Mock的问题

解决NATS订阅无法Mock的测试方案

核心思路是依赖抽象而非具体实现,通过定义抽象接口隔离NATS的具体类型,这样就能在测试中轻松Mock这些接口。

步骤1:定义抽象接口

把业务代码中用到的NATS核心能力抽成接口,包括订阅上下文、订阅实例、消息处理三个层面:

// 抽象NATS连接上下文,包含订阅方法
type NatsContext interface {
    SubscribeSync(subject string, opts ...nats.SubOpt) (NatsSubscription, error)
}

// 抽象订阅实例,包含消息获取和取消订阅方法
type NatsSubscription interface {
    NextMsgWithContext(ctx context.Context) (NatsMsg, error)
    Unsubscribe() error
}

// 抽象NATS消息,包含业务代码中用到的操作方法
type NatsMsg interface {
    Data() []byte
    InProgress()
    Ack()
    Nak()
    Term()
}

步骤2:修改业务代码依赖抽象

调整JetStreamConnection结构体,将原来的具体*nats.Conn类型替换为NatsContext接口:

type JetStreamConnection struct {
    context      NatsContext // 替换原*nats.Conn类型
    unmarshaller func(data []byte, v interface{}) error
    // 其他原有字段...
}

同时修改SubscribeMultiple中涉及消息的代码,将*nats.Msg替换为NatsMsg接口:

func (conn *JetStreamConnection) SubscribeMultiple(ctx context.Context, subject string,
    subscribers ...*SubscriptionCallback) error {

    callbacks := make(map[string]func(*pnats.NatsMessage) (func(context.Context), error))
    for _, subscriber := range subscribers {
        callbacks[subscriber.Category] = subscriber.Callback
    }

    fullSubject := fmt.Sprintf("%s.*", subject)
    sub, err := conn.context.SubscribeSync(fullSubject, nats.Context(ctx))
    if err != nil {
        return err
    }

loop:
    for {
        select {
        case <-ctx.Done():
            break loop
        default:
        }

        msg, err := sub.NextMsgWithContext(ctx)
        if err != nil {
            return err
        }

        msg.InProgress()

        var message pnats.NatsMessage
        if err := conn.unmarshaller(msg.Data(), &message); err != nil {
            msg.Term()
            return err
        }

        subscriber, ok := callbacks[message.Context.Category]
        if !ok {
            msg.Nak()
            continue
        }

        callback, err := subscriber(&message)
        if err == nil {
            msg.Ack()
        } else {
            msg.Nak()
            return err
        }

        callback(ctx)
    }

    if err := sub.Unsubscribe(); err != nil {
        return err
    }

    return nil
}

步骤3:实现Mock对象用于测试

手动编写Mock实现(或用testify/mock简化),控制返回值并记录方法调用:

// MockNatsMsg 模拟NATS消息,记录关键方法调用
type MockNatsMsg struct {
    DataBytes        []byte
    InProgressCalled bool
    AckCalled        bool
    NakCalled        bool
    TermCalled       bool
}

func (m *MockNatsMsg) Data() []byte                { return m.DataBytes }
func (m *MockNatsMsg) InProgress()                 { m.InProgressCalled = true }
func (m *MockNatsMsg) Ack()                        { m.AckCalled = true }
func (m *MockNatsMsg) Nak()                        { m.NakCalled = true }
func (m *MockNatsMsg) Term()                       { m.TermCalled = true }

// MockNatsSubscription 模拟订阅实例
type MockNatsSubscription struct {
    NextMsgFunc     func(ctx context.Context) (NatsMsg, error)
    UnsubscribeFunc func() error
}

func (m *MockNatsSubscription) NextMsgWithContext(ctx context.Context) (NatsMsg, error) {
    if m.NextMsgFunc != nil {
        return m.NextMsgFunc(ctx)
    }
    return nil, nil
}

func (m *MockNatsSubscription) Unsubscribe() error {
    if m.UnsubscribeFunc != nil {
        return m.UnsubscribeFunc()
    }
    return nil
}

// MockNatsContext 模拟NATS连接上下文
type MockNatsContext struct {
    SubscribeSyncFunc func(subject string, opts ...nats.SubOpt) (NatsSubscription, error)
}

func (m *MockNatsContext) SubscribeSync(subject string, opts ...nats.SubOpt) (NatsSubscription, error) {
    if m.SubscribeSyncFunc != nil {
        return m.SubscribeSyncFunc(subject, opts...)
    }
    return nil, nil
}

步骤4:编写测试用例

注入Mock对象,验证业务逻辑的正确性:

func TestSubscribeMultiple_RouteSuccess(t *testing.T) {
    // 准备测试消息数据
    testMsgData := []byte(`{"Context":{"Category":"test"},"Payload":"demo"}`)
    mockMsg := &MockNatsMsg{DataBytes: testMsgData}

    // 模拟订阅:返回测试消息,超时后触发ctx.Done退出循环
    mockSub := &MockNatsSubscription{
        NextMsgFunc: func(ctx context.Context) (NatsMsg, error) {
            select {
            case <-ctx.Done():
                return nil, ctx.Err()
            default:
                return mockMsg, nil
            }
        },
        UnsubscribeFunc: func() error { return nil },
    }

    // 模拟NATS上下文:验证订阅主题是否正确
    mockCtx := &MockNatsContext{
        SubscribeSyncFunc: func(subject string, opts ...nats.SubOpt) (NatsSubscription, error) {
            if subject != "test.*" {
                return nil, fmt.Errorf("invalid subject: %s", subject)
            }
            return mockSub, nil
        },
    }

    // 准备测试回调:标记是否被执行
    callbackExecuted := false
    testSubscriber := &SubscriptionCallback{
        Category: "test",
        Callback: func(msg *pnats.NatsMessage) (func(context.Context), error) {
            callbackExecuted = true
            return func(ctx context.Context) {}, nil
        },
    }

    // 创建测试用的JetStreamConnection实例
    conn := &JetStreamConnection{
        context:      mockCtx,
        unmarshaller: json.Unmarshal,
    }

    // 带超时的上下文,确保测试能自动结束
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    defer cancel()

    // 执行测试方法
    err := conn.SubscribeMultiple(ctx, "test", testSubscriber)

    // 验证结果
    if err != nil {
        t.Fatalf("SubscribeMultiple failed: %v", err)
    }
    if !callbackExecuted {
        t.Error("expected subscriber callback to be executed")
    }
    if !mockMsg.InProgressCalled {
        t.Error("expected msg.InProgress() to be called")
    }
    if !mockMsg.AckCalled {
        t.Error("expected msg.Ack() to be called on success")
    }
}

内容的提问来源于stack exchange,提问作者Woody1193

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 06:06:07