using System.Text;
using Dpz.Core.MessageQueue.Abstractions;
using Dpz.Core.MessageQueue.RabbitMQ;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using RabbitMQ.Client;

namespace Dpz.Core.MessageQueue.Test.RabbitMQ;

/// <summary>
/// 验证 <see cref="RabbitMQRawPublisher"/> 的核心契约:
/// 调用连接工厂创建 Channel、按指定 exchange/routingKey 投递、设置消息属性、关闭 Channel。
/// </summary>
public class RabbitMQRawPublisherTests
{
    [Fact]
    public async Task PublishRawAsync_ShouldInvokeBasicPublishWithProvidedParameters()
    {
        var (channel, factory) = BuildChannelAndFactory();

        var publisher = new RabbitMQRawPublisher(
            factory.Object,
            NullLogger<RabbitMQRawPublisher>.Instance
        );

        const string payload = "{\"foo\":\"bar\"}";
        await publisher.PublishRawAsync("ex.test", "rk.test", "msg-123", payload);

        factory.Verify(x => x.CreateChannelAsync(), Times.Once);

        BasicProperties? captured = null;
        ReadOnlyMemory<byte> capturedBody = default;
        channel.Verify(
            c =>
                c.BasicPublishAsync(
                    "ex.test",
                    "rk.test",
                    false,
                    It.IsAny<BasicProperties>(),
                    It.IsAny<ReadOnlyMemory<byte>>(),
                    It.IsAny<CancellationToken>()
                ),
            Times.Once
        );

        // 重新捕获以便断言 properties 字段
        channel.Invocations.Clear();
        channel
            .Setup(c =>
                c.BasicPublishAsync(
                    It.IsAny<string>(),
                    It.IsAny<string>(),
                    It.IsAny<bool>(),
                    It.IsAny<BasicProperties>(),
                    It.IsAny<ReadOnlyMemory<byte>>(),
                    It.IsAny<CancellationToken>()
                )
            )
            .Callback<
                string,
                string,
                bool,
                BasicProperties,
                ReadOnlyMemory<byte>,
                CancellationToken
            >(
                (_, _, _, props, body, _) =>
                {
                    captured = props;
                    capturedBody = body;
                }
            )
            .Returns(ValueTask.CompletedTask);

        await publisher.PublishRawAsync("ex.test", "rk.test", "msg-456", payload);

        Assert.NotNull(captured);
        Assert.Equal("msg-456", captured!.MessageId);
        Assert.True(captured.Persistent);
        Assert.Equal(DeliveryModes.Persistent, captured.DeliveryMode);
        Assert.Equal("application/json", captured.ContentType);
        Assert.Equal(payload, Encoding.UTF8.GetString(capturedBody.Span));
    }

    [Fact]
    public async Task PublishRawAsync_ShouldDisposeChannelAfterPublish()
    {
        var (channel, factory) = BuildChannelAndFactory();

        var publisher = new RabbitMQRawPublisher(
            factory.Object,
            NullLogger<RabbitMQRawPublisher>.Instance
        );

        await publisher.PublishRawAsync("ex", "rk", "id", "{}");

        // RabbitMQRawPublisher 使用 await using 管理 Channel,调用结束应释放
        channel.Verify(c => c.DisposeAsync(), Times.Once);
    }

    [Fact]
    public async Task PublishRawAsync_ShouldPropagate_WhenBasicPublishThrows()
    {
        var (channel, factory) = BuildChannelAndFactory();
        channel
            .Setup(c =>
                c.BasicPublishAsync(
                    It.IsAny<string>(),
                    It.IsAny<string>(),
                    It.IsAny<bool>(),
                    It.IsAny<BasicProperties>(),
                    It.IsAny<ReadOnlyMemory<byte>>(),
                    It.IsAny<CancellationToken>()
                )
            )
            .ThrowsAsync(new InvalidOperationException("publish failed"));

        var publisher = new RabbitMQRawPublisher(
            factory.Object,
            NullLogger<RabbitMQRawPublisher>.Instance
        );

        await Assert.ThrowsAsync<InvalidOperationException>(() =>
            publisher.PublishRawAsync("ex", "rk", "id", "{}")
        );
    }

    private static (
        Mock<IChannel> channel,
        Mock<IRabbitMQConnectionFactory> factory
    ) BuildChannelAndFactory()
    {
        var channel = new Mock<IChannel>();
        channel
            .Setup(c =>
                c.BasicPublishAsync(
                    It.IsAny<string>(),
                    It.IsAny<string>(),
                    It.IsAny<bool>(),
                    It.IsAny<BasicProperties>(),
                    It.IsAny<ReadOnlyMemory<byte>>(),
                    It.IsAny<CancellationToken>()
                )
            )
            .Returns(ValueTask.CompletedTask);
        channel.Setup(c => c.DisposeAsync()).Returns(ValueTask.CompletedTask);

        var factory = new Mock<IRabbitMQConnectionFactory>();
        factory.Setup(f => f.CreateChannelAsync()).ReturnsAsync(channel.Object);

        return (channel, factory);
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

这是一个针对 RabbitMQRawPublisher 类的单元测试文件。让我逐一解释这个测试类的结构和功能:

测试类概述

RabbitMQRawPublisherTests 类用于验证 RabbitMQRawPublisher 的核心功能契约,确保该类能够正确地:

  • 创建 RabbitMQ 连接通道
  • 按指定参数发布消息
  • 设置正确的消息属性
  • 正确关闭通道资源

主要测试方法

1. PublishRawAsync_ShouldInvokeBasicPublishWithProvidedParameters()

测试目标:验证发布方法是否正确调用底层 RabbitMQ API 并传递正确参数

测试流程

// 1. 创建模拟对象
var (channel, factory) = BuildChannelAndFactory();
var publisher = new RabbitMQRawPublisher(factory.Object, NullLogger);

// 2. 调用发布方法
await publisher.PublishRawAsync("ex.test", "rk.test", "msg-123", payload);

// 3. 验证调用行为
factory.Verify(x => x.CreateChannelAsync(), Times.Once);
channel.Verify(c => c.BasicPublishAsync(...), Times.Once);

重点验证

  • 消息属性设置正确(MessageId、持久化、内容类型)
  • 消息体编码正确(UTF-8)
  • 交换器和路由键参数传递正确

2. PublishRawAsync_ShouldDisposeChannelAfterPublish()

测试目标:确保资源正确释放

await publisher.PublishRawAsync("ex", "rk", "id", "{}");
// 验证 Channel 被正确释放
channel.Verify(c => c.DisposeAsync(), Times.Once);

3. PublishRawAsync_ShouldPropagate_WhenBasicPublishThrows()

测试目标:验证异常处理机制

// 模拟底层方法抛出异常
channel.Setup(...).ThrowsAsync(new InvalidOperationException("publish failed"));

// 验证异常被正确传播
await Assert.ThrowsAsync<InvalidOperationException>(() => 
    publisher.PublishRawAsync("ex", "rk", "id", "{}")
);

辅助方法

BuildChannelAndFactory()

创建测试所需的模拟对象:

private static (Mock<IChannel> channel, Mock<IRabbitMQConnectionFactory> factory) BuildChannelAndFactory()
{
    // 创建 Channel 模拟对象,设置默认行为
    var channel = new Mock<IChannel>();
    channel.Setup(...).Returns(ValueTask.CompletedTask);
    
    // 创建工厂模拟对象
    var factory = new Mock<IRabbitMQConnectionFactory>();
    factory.Setup(f => f.CreateChannelAsync()).ReturnsAsync(channel.Object);
    
    return (channel, factory);
}

测试特色

  1. 使用 Moq 框架:创建模拟对象来隔离依赖
  2. 异步测试:所有测试方法都是异步的,符合现代 C# 最佳实践
  3. 行为验证:不仅测试结果,还验证方法调用次数和参数
  4. 资源管理测试:确保 await using 模式正确工作
  5. 异常传播测试:验证错误处理机制

测试价值

这些测试确保了 RabbitMQRawPublisher 类:

  • 正确集成 RabbitMQ 客户端库
  • 遵循资源管理最佳实践
  • 具有合适的错误处理机制
  • 消息属性设置符合预期

这是一个结构良好的单元测试套件,遵循了测试驱动开发的最佳实践。

评论加载中...