簡體   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