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?
TL;DR
Ran into the same issue and solved it by utilizing reflection to gain access to private members of the class
Message.SystemPropertiesCollection
.More detailed explanation
In order to avoid that
message.SystemProperties.LockToken
throws anInvalidOperationException
you have to set theMessage.SystemPropertiesCollection.SequenceNumber
property to something else than -1. But the setter of that property is private, the classMessage.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 fakeMessage
objects that don't throw.Sample code
This helper method generates
Message
objects that don't throw.