I have the following class I wish to create unit tests for:
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
.
My first attempt to mock this out was to use 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"....
Then I thought I would try to manually mock out the 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)
.
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:
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.
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.
So, now I'm back to square one.
Any ideas how I can create good unit tests for ServiceBusClient?
Ran into the same issue and solved it by utilizing reflection to gain access to private members of the class Message.SystemPropertiesCollection
.
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. 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. 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.
This helper method generates Message
objects that don't throw.
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.
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
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. As documented here: 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. However, your constructor is expecting 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:
var serviceBusClient = new ServiceBusClient(
new Mock<IMessageReceiver>().Object,
0
);
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.