繁体   English   中英

在 C# 中,是否可以模拟 IMessageReceiver 和相关类进行单元测试?

[英]In C#, is it possible to mock out IMessageReceiver and related classes for unit testing?

我有以下 class 我希望为其创建单元测试:

    public class ServiceBusClient {
        private readonly IMessageReceiver messageReceiver;
        private readonly int maximumMessages;

        public ServiceBusClient(IMessageReceiver messageReceiver, int maximumMessages) {
            this.messageReceiver = messageReceiver;
            this.maximumMessages = maximumMessages;
        }

        public async Task<List<EnergyUser>> ReceiveEnergyUsersAsync() {
            List<EnergyUser> energyUsers = new List<EnergyUser>();
            List<string> lockTokens = new List<string>();
            
            this.ReceiveMessages()
                .ForEach((message) => {
                    if (message.Body != null) {
                        energyUsers.Add(JsonConvert.DeserializeObject<EnergyUser>(Encoding.UTF8.GetString(message.Body)));
                    }
                    lockTokens.Add(message.SystemProperties.LockToken);
                });

            _ = this.messageReceiver.CompleteAsync(lockTokens);
            return await Task.FromResult(energyUsers);
        }

        private List<Message> ReceiveMessages() {
            return this.messageReceiver.ReceiveAsync(this.maximumMessages)
                            .GetAwaiter()
                            .GetResult()
                            .ToList();
        }
    }

可以观察到它依赖于Microsoft.Azure.ServiceBus.Core.IMessageReceiver

我第一次尝试模拟这个是使用Moq 我本来希望如果我创建一个new Mock<IMessageReceiver>() ,我应该能够将它注入public ServiceBusClient(IMessageReceiver messageReceiver, int maximumMessages) ,但是编译器告诉我“错误 CS1503 参数 1:无法从 ' Moq.Mock<Microsoft.Azure.ServiceBus.Core.IMessageReceiver>' 到 'Microsoft.Azure.ServiceBus.Core.IMessageReceiver"....

然后我想我会尝试手动模拟出 class:

    internal class MockMessageReceiver : IMessageReceiver {
        public int ReceivedMaxMessgeCount { get; set; }
        public IList<Message> ReturnMessages { get; set; }
        Task<IList<Message>> IMessageReceiver.ReceiveAsync(int maxMessageCount) {
            this.ReceivedMaxMessgeCount = maxMessageCount;
            return Task.FromResult(this.ReturnMessages);
        }

        public IEnumerable<string> ReceivedLockTokens { get; set; }
        Task IMessageReceiver.CompleteAsync(IEnumerable<string> lockTokens) {
            this.ReceivedLockTokens = lockTokens;
            return Task.Delay(1);
        }

        // Many functions which do nothing just to satisfy the bloated interface.
}

这将允许我成功提供消息,除了我提供的消息不包括 SystemProperties,因此 ServiceBusClient 将在lockTokens.Add(message.SystemProperties.LockToken)处引发错误。

事实证明, Microsoft.Azure.ServiceBus.Message实现没有为public SystemPropertiesCollection SystemProperties提供设置器,因此要设置它(除非有人有更好的方法),我需要创建自己的 Message 实现:

    public class MockMessage : Message {
        public MockMessage(byte[] body) => base.Body = body;

        public new SystemPropertiesCollection SystemProperties {
            get { return this.SystemProperties; }
            set { this.SystemProperties = value; }
        }
    }

现在,我可以初始化 SystemPropertiesCollection,但问题是 SystemPropertiesCollection 中没有任何属性实际上包含设置,所以我的测试仍然会失败。

然后我想:让我们为“SystemPropertiesCollection”创建一个模拟(不要介意我们开始在“太多模拟”的危险水域中游泳......但是当我尝试扩展这个 class 时,我的编译器抱怨因为 SystemPropertiesCollection 是实际上是一个密封的 class,所以我不能扩展它。

所以,现在我回到第一方。

有什么想法可以为 ServiceBusClient 创建好的单元测试吗?

TL;博士

遇到同样的问题并通过利用反射来访问 class Message.SystemPropertiesCollection的私有成员来解决它。

更详细的解释

为了避免message.SystemProperties.LockToken引发InvalidOperationException ,您必须将Message.SystemPropertiesCollection.SequenceNumber属性设置为 -1 以外的值。 但是该属性的设置器是私有的,class Message.SystemPropertiesCollection是密封的,并且没有其他间接方法可以在不通过服务总线实际发送消息的情况下设置序列号。 您不想在单元测试中这样做。

但是通过反射欺骗调用Message.SystemPropertiesCollection.SequenceNumber的私有设置器解决了这个障碍,让我伪造了不会抛出的Message对象。

示例代码

此辅助方法生成不抛出的Message对象。

private static Message GenerateSaneMessage()
{
    var message = new Message();
    var sysCollectionType = typeof(Message.SystemPropertiesCollection);
    sysCollectionType.GetProperty(nameof(Message.SystemPropertiesCollection.SequenceNumber))!
        .SetValue(message.SystemProperties, 0);
    return message;
}

虽然不是理想的解决方案,但我在当前打开的 GitHub 问题中找到了一些建议。

一种是有一个 RunInternalAsync 方法,该方法采用一个调用相同实现的 IReceiverClient messageReceiver https://github.com/Azure/azure-functions-servicebus-extension/issues/69

该问题帖子中建议的代码:

// Called by the Service Bus binding.
[FunctionName("Foo")]
public Task RunAsync(
    [ServiceBusTrigger] Message serviceBusMessage,
    MessageReceiver messageReceiver) => RunInternalAsync(serviceBusMessage, messageReceiver);

// Called by unit tests.
internal Task RunInternalAsync(
    Message serviceBusMessage, 
    IReceiverClient messageReceiver { 
    // Implementation
}

我最终选择了一个可以在单元测试中设置的属性,然后你在 function 代码中适当地设置了 map。 如此处所述: https://github.com/Azure/azure-webjobs-sdk/pull/2218#issuecomment-647584346

示例代码:

//Azure Function:
public class ServiceReportingFunction
{
    private ILogger<ServiceReportingFunction> Logger { get; }

    //Needed for unit testing until 'IMessageReceiver messageReceiver' is supported as argument in the Run Method.
    public IMessageReceiver MessageReceiver { get; set; }


    public ServiceReportingFunction(ILogger<ServiceReportingFunction> logger)
    {
        Logger = logger;
    }

    [FunctionName("ServiceReportingFunction")]
    public async Task Run([ServiceBusTrigger("%ServiceReportingQueueName%", Connection = "ServiceBusConnectionString")]Message message, MessageReceiver messageReceiver)
    {
        if (MessageReceiver == null)
            MessageReceiver = messageReceiver;

        ...
    }
}

//Unit (Xunit) test:
public class ServiceReportingFunctionTests
{
    [Fact]
    public async void Test_ServiceReportingFunction()
    {
        var logger = A.Fake<ILogger<ServiceReportingFunction>>();
        var messageReceiver = A.Fake<IMessageReceiver>();

        var function = new ServiceReportingFunction(logger);
        function.MessageReceiver = messageReceiver;
        
        ....

        await function.Run(message, null);

       ....
    }
}

这个new Mock<IMessageReceiver>()将返回一个 Mock 实例。 但是,您的构造函数需要 IMessageReceiver。 这就是编译器显示错误的原因。

您必须通过调用模拟上的Object属性来获取实例,如下所示:

var serviceBusClient = new ServiceBusClient(
    new Mock<IMessageReceiver>().Object, 
    0
);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM