using Dpz.Core.Entity.Base;
using Dpz.Core.MessageQueue.Abstractions;
using Dpz.Core.MessageQueue.Configuration;
using Dpz.Core.MessageQueue.Extensions;
using Dpz.Core.MessageQueue.Models;
using Dpz.Core.MessageQueue.RabbitMQ;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

namespace Dpz.Core.MessageQueue.Test.Extensions;

public class MessageQueueServiceExtensionsTests
{
    [Fact]
    public async Task AddRabbitMQ_ShouldRegisterCoreServicesAndOptions()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(
                new Dictionary<string, string?>
                {
                    ["RabbitMQ:HostName"] = "mq.test.local",
                    ["RabbitMQ:Port"] = "5673",
                    ["RabbitMQ:UserName"] = "u",
                    ["RabbitMQ:Password"] = "p",
                    ["RabbitMQ:VirtualHost"] = "/v",
                }
            )
            .Build();

        services.AddRabbitMQ(configuration);

        await using var provider = services.BuildServiceProvider();

        var options = provider.GetRequiredService<IOptions<RabbitMQOptions>>().Value;
        Assert.Equal("mq.test.local", options.HostName);
        Assert.Equal(5673, options.Port);

        var routing = provider.GetRequiredService<IMessageRoutingConvention>();
        Assert.IsType<DefaultMessageRoutingConvention>(routing);

        var factory = provider.GetRequiredService<IRabbitMQConnectionFactory>();
        Assert.IsType<RabbitMQConnectionFactory>(factory);

        var publisher = provider.GetRequiredService<IMessagePublisher<TestMessage>>();
        Assert.IsType<RabbitMQPublisher<TestMessage>>(publisher);
    }

    [Fact]
    public void AddMessageConsumer_ShouldRegisterHandlerAndHostedService()
    {
        var services = new ServiceCollection();

        services.AddMessageConsumer<TestMessage, TestMessageHandler>();

        var handlerDescriptor = services.SingleOrDefault(x =>
            x.ServiceType == typeof(IMessageHandler<TestMessage>)
        );
        Assert.NotNull(handlerDescriptor);
        Assert.Equal(ServiceLifetime.Scoped, handlerDescriptor.Lifetime);
        Assert.Equal(typeof(TestMessageHandler), handlerDescriptor.ImplementationType);

        var hostedServiceDescriptor = services.LastOrDefault(x =>
            x.ServiceType == typeof(IHostedService)
        );
        Assert.NotNull(hostedServiceDescriptor);
        Assert.Equal(
            typeof(RabbitMQConsumerBackgroundService<TestMessage>),
            hostedServiceDescriptor.ImplementationType
        );
    }

    [Fact]
    public void AddMessageConsumerWithResult_ShouldRegisterHandlerAndHostedService()
    {
        var services = new ServiceCollection();

        services.AddMessageConsumer<
            TestMessage,
            TestMessageResultHandler,
            MessageHandlerResult<string>
        >();

        var handlerDescriptor = services.SingleOrDefault(x =>
            x.ServiceType == typeof(IMessageHandler<TestMessage, MessageHandlerResult<string>>)
        );
        Assert.NotNull(handlerDescriptor);
        Assert.Equal(ServiceLifetime.Scoped, handlerDescriptor.Lifetime);
        Assert.Equal(typeof(TestMessageResultHandler), handlerDescriptor.ImplementationType);

        var hostedServiceDescriptor = services.LastOrDefault(x =>
            x.ServiceType == typeof(IHostedService)
        );
        Assert.NotNull(hostedServiceDescriptor);
        Assert.Equal(
            typeof(RabbitMQConsumerBackgroundServiceWithResult<
                TestMessage,
                MessageHandlerResult<string>
            >),
            hostedServiceDescriptor.ImplementationType
        );
    }

    [Fact]
    public void AddCustomRoutingConvention_ShouldReplaceRoutingConvention()
    {
        var services = new ServiceCollection();

        services.AddCustomRoutingConvention<CustomRoutingConvention>();

        using var provider = services.BuildServiceProvider();
        var routing = provider.GetRequiredService<IMessageRoutingConvention>();

        Assert.IsType<CustomRoutingConvention>(routing);
    }

    [Fact]
    public async Task AddRabbitMQ_ShouldRegisterNullOutboxStoreAndRetryPublisherByDefault()
    {
        var services = new ServiceCollection();
        services.AddLogging();
        var configuration = new ConfigurationBuilder()
            .AddInMemoryCollection(new Dictionary<string, string?> { ["RabbitMQ:HostName"] = "h" })
            .Build();

        services.AddRabbitMQ(configuration);

        await using var provider = services.BuildServiceProvider();

        var store = provider.GetRequiredService<IMessageOutboxStore>();
        Assert.IsType<NullMessageOutboxStore>(store);

        var retryPublisher = provider.GetRequiredService<IMessageOutboxRetryPublisher>();
        // NullMessageOutboxRetryPublisher 是 internal,仅做不为 null 校验
        Assert.NotNull(retryPublisher);
        Assert.Equal(
            "Dpz.Core.MessageQueue.RabbitMQ.NullMessageOutboxRetryPublisher",
            retryPublisher.GetType().FullName
        );

        // 默认 Null 实现的重试发布也应保持空操作语义
        await retryPublisher.PublishRawAsync("ex", "rk", "id", "{}");
    }

    [Fact]
    public void AddBatchTracking_ShouldRegisterBatchTracker()
    {
        var services = new ServiceCollection();

        services.AddBatchTracking();

        var descriptor = services.SingleOrDefault(x => x.ServiceType == typeof(IBatchTracker));
        Assert.NotNull(descriptor);
        Assert.Equal(ServiceLifetime.Singleton, descriptor.Lifetime);
        Assert.Equal(typeof(DistributedBatchTracker), descriptor.ImplementationType);
    }

    [Fact]
    public void AddMessageOutboxRetryWorker_ShouldRegisterRetryServiceAndHostedService()
    {
        var services = new ServiceCollection();

        services.AddMessageOutboxRetryWorker();

        var retryServiceDescriptor = services.SingleOrDefault(x =>
            x.ServiceType == typeof(IMessageOutboxRetryService)
        );
        Assert.NotNull(retryServiceDescriptor);
        Assert.Equal(ServiceLifetime.Singleton, retryServiceDescriptor.Lifetime);
        Assert.Equal(typeof(MessageOutboxRetryService), retryServiceDescriptor.ImplementationType);

        var hostedServiceDescriptor = services.LastOrDefault(x =>
            x.ServiceType == typeof(IHostedService)
        );
        Assert.NotNull(hostedServiceDescriptor);
        Assert.Equal(
            typeof(MessageOutboxRetryBackgroundService),
            hostedServiceDescriptor.ImplementationType
        );
    }

    private sealed class TestMessage : MessageBase { }

    private sealed class TestMessageHandler : IMessageHandler<TestMessage>
    {
        public Task<MessageHandlerResult> HandleAsync(
            TestMessage message,
            CancellationToken cancellationToken = default
        )
        {
            return Task.FromResult(MessageHandlerResult.Ok());
        }
    }

    private sealed class TestMessageResultHandler
        : IMessageHandler<TestMessage, MessageHandlerResult<string>>
    {
        public Task<MessageHandlerResult<string>> HandleAsync(
            TestMessage message,
            CancellationToken cancellationToken = default
        )
        {
            return Task.FromResult(MessageHandlerResult<string>.Ok("ok"));
        }
    }

    private sealed class CustomRoutingConvention : IMessageRoutingConvention
    {
        public string GetExchangeName<TMessage>() => "custom-exchange";

        public string GetQueueName<TMessage>() => "custom-queue";

        public string GetRoutingKey<TMessage>() => "custom.routing";

        public Enums.ExchangeType GetExchangeType<TMessage>() => Enums.ExchangeType.Fanout;
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个针对 消息队列服务扩展方法 的单元测试类,主要测试 RabbitMQ 消息队列的依赖注入配置功能。

整体结构

该测试类 MessageQueueServiceExtensionsTests 包含多个测试用例,验证消息队列框架的各种扩展方法是否正确注册了所需的服务。


各测试方法详解

1. AddRabbitMQ_ShouldRegisterCoreServicesAndOptions

测试目标: 验证 AddRabbitMQ 扩展方法是否正确注册了核心服务和配置选项

测试内容:

  • 从内存配置中加载 RabbitMQ 连接参数(主机名、端口、用户名、密码、虚拟主机)
  • 验证 RabbitMQOptions 配置对象是否正确注入
  • 验证路由约定(IMessageRoutingConvention)默认实现为 DefaultMessageRoutingConvention
  • 验证连接工厂(IRabbitMQConnectionFactory)注册为 RabbitMQConnectionFactory
  • 验证消息发布器(IMessagePublisher<TestMessage>)注册为 RabbitMQPublisher<TestMessage>

2. AddMessageConsumer_ShouldRegisterHandlerAndHostedService

测试目标: 验证无返回值的消息消费者注册

测试内容:

  • 调用 AddMessageConsumer<TestMessage, TestMessageHandler>()
  • 验证 IMessageHandler<TestMessage> 接口注册为 Scoped 生命周期
  • 验证实现类型为 TestMessageHandler
  • 验证后台服务 RabbitMQConsumerBackgroundService<TestMessage> 被注册为 IHostedService

3. AddMessageConsumerWithResult_ShouldRegisterHandlerAndHostedService

测试目标: 验证带返回值的消息消费者注册

测试内容:

  • 调用泛型方法注册带结果类型 MessageHandlerResult<string> 的消费者
  • 验证 IMessageHandler<TestMessage, MessageHandlerResult<string>> 接口注册
  • 验证后台服务类型为 RabbitMQConsumerBackgroundServiceWithResult<TestMessage, MessageHandlerResult<string>>

4. AddCustomRoutingConvention_ShouldReplaceRoutingConvention

测试目标: 验证自定义路由约定的替换机制

测试内容:

  • 注册自定义路由约定 CustomRoutingConvention
  • 验证依赖注入容器返回的实例类型正确

5. AddRabbitMQ_ShouldRegisterNullOutboxStoreAndRetryPublisherByDefault

测试目标: 验证默认情况下的 Outbox 模式实现

测试内容:

  • 验证默认注册的 IMessageOutboxStore 为空实现 NullMessageOutboxStore
  • 验证默认的重试发布器为内部类 NullMessageOutboxRetryPublisher
  • 测试重试发布方法可安全调用(空操作)

设计意图: 采用空对象模式,未启用 Outbox 功能时不抛异常


6. AddBatchTracking_ShouldRegisterBatchTracker

测试目标: 验证批量处理追踪器的注册

测试内容:

  • 验证 IBatchTracker 接口注册为 Singleton 单例
  • 实现类型为 DistributedBatchTracker

7. AddMessageOutboxRetryWorker_ShouldRegisterRetryServiceAndHostedService

测试目标: 验证 Outbox 重试工作器的注册

测试内容:

  • 验证重试服务 IMessageOutboxRetryService 注册为 Singleton
  • 实现类型为 MessageOutboxRetryService
  • 验证后台工作服务 MessageOutboxRetryBackgroundService 被注册

辅助测试类

TestMessage

简单的测试消息类,继承自 MessageBase

TestMessageHandler

无返回值的消息处理器,返回 MessageHandlerResult.Ok()

TestMessageResultHandler

带返回值的消息处理器,返回 MessageHandlerResult<string>.Ok("ok")

CustomRoutingConvention

自定义路由约定实现,固定返回自定义的交换机、队列和路由键名称


总结

这个测试类全面覆盖了消息队列框架的依赖注入配置验证,包括:

  • ✅ RabbitMQ 核心组件注册
  • ✅ 消息消费者生命周期管理
  • ✅ 自定义扩展点(路由约定)
  • ✅ 可靠性保障机制(Outbox 模式、重试机制)
  • ✅ 批量处理追踪

测试采用标准的 xUnit 框架,使用 ServiceCollection 模拟 ASP.NET Core 的依赖注入容器。

评论加载中...