简体   繁体   English

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

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

I have the following class I wish to create unit tests for:我有以下 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();
        }
    }

It will be observed that it is dependent upon Microsoft.Azure.ServiceBus.Core.IMessageReceiver .可以观察到它依赖于Microsoft.Azure.ServiceBus.Core.IMessageReceiver

My first attempt to mock this out was to use Moq .我第一次尝试模拟这个是使用Moq I would have expected if I create a new Mock<IMessageReceiver>() , I should be able to inject it into public ServiceBusClient(IMessageReceiver messageReceiver, int maximumMessages) , but instead the compiler tells me "Error CS1503 Argument 1: cannot convert from 'Moq.Mock<Microsoft.Azure.ServiceBus.Core.IMessageReceiver>' to 'Microsoft.Azure.ServiceBus.Core.IMessageReceiver"....我本来希望如果我创建一个new Mock<IMessageReceiver>() ,我应该能够将它注入public ServiceBusClient(IMessageReceiver messageReceiver, int maximumMessages) ,但是编译器告诉我“错误 CS1503 参数 1:无法从 ' Moq.Mock<Microsoft.Azure.ServiceBus.Core.IMessageReceiver>' 到 'Microsoft.Azure.ServiceBus.Core.IMessageReceiver"....

Then I thought I would try to manually mock out the class:然后我想我会尝试手动模拟出 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.
}

This will allow me to successfully provide messages EXCEPT the messages I provide don't include SystemProperties, so ServiceBusClient will throw an error at lockTokens.Add(message.SystemProperties.LockToken) .这将允许我成功提供消息,除了我提供的消息不包括 SystemProperties,因此 ServiceBusClient 将在lockTokens.Add(message.SystemProperties.LockToken)处引发错误。

It turns out that the Microsoft.Azure.ServiceBus.Message implementation does not provide a setter for public SystemPropertiesCollection SystemProperties , so to set this (unless someone has a better way), I need to create my own implementation of Message:事实证明, 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; }
        }
    }

Now, I can initialize SystemPropertiesCollection, BUT the problem becomes that no property in SystemPropertiesCollection actually includes a setting, so my tests will still fail.现在,我可以初始化 SystemPropertiesCollection,但问题是 SystemPropertiesCollection 中没有任何属性实际上包含设置,所以我的测试仍然会失败。

Then I thought: Let's create a mock for "SystemPropertiesCollection" (never mind that we are starting to swim in the dangerous waters of "too much mock".... but when I try to extend this class, my compiler complains because SystemPropertiesCollection is actually a sealed class, so I can't extend it.然后我想:让我们为“SystemPropertiesCollection”创建一个模拟(不要介意我们开始在“太多模拟”的危险水域中游泳......但是当我尝试扩展这个 class 时,我的编译器抱怨因为 SystemPropertiesCollection 是实际上是一个密封的 class,所以我不能扩展它。

So, now I'm back to square one.所以,现在我回到第一方。

Any ideas how I can create good unit tests for ServiceBusClient?有什么想法可以为 ServiceBusClient 创建好的单元测试吗?

TL;DR TL;博士

Ran into the same issue and solved it by utilizing reflection to gain access to private members of the class Message.SystemPropertiesCollection .遇到同样的问题并通过利用反射来访问 class Message.SystemPropertiesCollection的私有成员来解决它。

More detailed explanation更详细的解释

In order to avoid that message.SystemProperties.LockToken throws an InvalidOperationException you have to set the Message.SystemPropertiesCollection.SequenceNumber property to something else than -1.为了避免message.SystemProperties.LockToken引发InvalidOperationException ,您必须将Message.SystemPropertiesCollection.SequenceNumber属性设置为 -1 以外的值。 But the setter of that property is private, the class Message.SystemPropertiesCollection is sealed and there is no other indirect way of setting the sequence number without actually sending a message over a service bus.但是该属性的设置器是私有的,class Message.SystemPropertiesCollection是密封的,并且没有其他间接方法可以在不通过服务总线实际发送消息的情况下设置序列号。 Which you don't want to do in a unit test.您不想在单元测试中这样做。

But cheating with reflection to call the private setter of Message.SystemPropertiesCollection.SequenceNumber resolved that hurdle and let me fake Message objects that don't throw.但是通过反射欺骗调用Message.SystemPropertiesCollection.SequenceNumber的私有设置器解决了这个障碍,让我伪造了不会抛出的Message对象。

Sample code示例代码

This helper method generates Message objects that don't throw.此辅助方法生成不抛出的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;
}

Whilst not ideal solutions I found a few suggestions in currently open GitHub issues.虽然不是理想的解决方案,但我在当前打开的 GitHub 问题中找到了一些建议。

One was to have a RunInternalAsync method that takes an IReceiverClient messageReceiver that calls the same implementation https://github.com/Azure/azure-functions-servicebus-extension/issues/69一种是有一个 RunInternalAsync 方法,该方法采用一个调用相同实现的 IReceiverClient messageReceiver https://github.com/Azure/azure-functions-servicebus-extension/issues/69

Code suggested in that issue post:该问题帖子中建议的代码:

// 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
}

I ended up going with a property that could be set in the unit test, and then you map that appropriately in your function code.我最终选择了一个可以在单元测试中设置的属性,然后你在 function 代码中适当地设置了 map。 As documented here: https://github.com/Azure/azure-webjobs-sdk/pull/2218#issuecomment-647584346如此处所述: https://github.com/Azure/azure-webjobs-sdk/pull/2218#issuecomment-647584346

Sample code:示例代码:

//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);

       ....
    }
}

This new Mock<IMessageReceiver>() will return a Mock instance.这个new Mock<IMessageReceiver>()将返回一个 Mock 实例。 However, your constructor is expecting IMessageReceiver.但是,您的构造函数需要 IMessageReceiver。 That's why the compiler shows an error.这就是编译器显示错误的原因。

You have to get the instance by calling the Object property on the mock like this:您必须通过调用模拟上的Object属性来获取实例,如下所示:

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

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

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