Introduction
As some of you might have heard, ASP.NET Core 2.1.0 went live at the end of last month. One of the features that comes with this release is also the release of SignalR which for those who do not know about it, is a library that allows for two-way communication between a web server and a browser over HTTP or WebSockets.
I won’t be going into details as to how SignalR works because there’s quite a bit of documentation on it provided in the link above, including a tutorial, so in this post, we will be having a look at how we can unit test a SignalR hub so that we can make sure that our server is sending out the right signals.
The code for this exercise can be found here.
The Setup
Creating the Web Project
For this post, we’re going to create a new ASP.NET Core 2.1 application (should work with all of the ASP.Core web application templates), with no authentication or any other details because we have no interest in those.
Creating the Test Project
Then we will create a .NET Core test project which will have a reference to the following Nuget packages:
I find that these are the bare minimum packages I work best with when unit testing.
Then we reference our own web application from the test project.
Creating the Hub
Now that we have the projects out of the way, let’s register a simple hub in our web application.
Let’s create a file in the web application called SimpleHub
and it will look like this:
namespace SignalRWebApp
{
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
public class SimpleHub : Hub
{
public override async Task OnConnectedAsync()
{
await Welcome();
await base.OnConnectedAsync();
}
public async Task Welcome()
{
await Clients.All.SendAsync("welcome", new[]
{ new HubMessage(), new HubMessage(), new HubMessage() });
}
}
}
For this, we will also create the class HubMessage
that is just a placeholder so that we don’t use anonymous objects and it looks like this:
namespace SignalRWebApp
{
public class HubMessage
{
}
}
This will just send a series of 3 messages to anyone who connects to the hub. I chose the arbitrary number 3 so that I can also test the content and length of messages sent by the server.
In Startup.cs, we add the following lines:
- In
ConfigureServices
, we add the line services.AddSignalR();
- In
Configure
before the line app.UseMvc
, we add the line app.UseSignalR(builder => builder.MapHub("/hub"));
With this, we now have a working hub to which clients can connect to.
Creating the Client Connection
To test this out, we will be using the install steps found here to install JavaScript SignalR so that we can use it in the browser and make our own script as follows:
@section Scripts
{
$(document).ready(() => {
const connection = new signalR.HubConnectionBuilder().withUrl("/hub").build();
connection.on("welcome", (messages) => {
alert(messages);
});
connection.start().catch(err => console.error(err.toString()));
});
}
And now, all we need to do is run the website and see that we get an alert with 3 objects.
The Test
Now we get to the interesting part, I will paste the test here and then break it down.
namespace SignalRWebApp.Tests
{
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Moq;
using NUnit.Framework;
[TestFixture]
public class Test
{
[Test]
public async Task SignalR_OnConnect_ShouldReturn3Messages()
{
Mock<IHubCallerClients> mockClients = new Mock<IHubCallerClients>();
Mock<IClientProxy> mockClientProxy = new Mock<IClientProxy>();
mockClients.Setup(clients => clients.All).Returns(mockClientProxy.Object);
SimpleHub simpleHub = new SimpleHub()
{
Clients = mockClients.Object
};
await simpleHub.Welcome();
mockClients.Verify(clients => clients.All, Times.Once);
mockClientProxy.Verify(
clientProxy => clientProxy.SendCoreAsync(
"welcome",
It.Is<object[]>(o => o != null && o.Length == 1 && ((object[])o[0]).Length == 3),
default(CancellationToken)),
Times.Once);
}
}
}
Now let’s break it down:
- In true testing fashion, the test is split up in 3 sections, arrange which handles the setup for the test, act which does the actual logic we want to test, and assert which tests that our logic actually behaved as we intended.
- SignalR hubs don’t really contain a lot of logic, all they do is to delegate the work onto
IHubCallerClients
which in turn when sending a message will delegate the call to an IClientProxy
- We then create a mock for both
IHubCallerClients
and IClientProxy
- On line 22, we set up the mock so that when the
All
property is called, then the instance of the IClientProxy
mock is returned. - We then create a
SimpleHub
and tell it to use our mock for its Clients
delegation. Now we have full control over the flow. - We do the call to
SimpleHub.Welcome
which starts the whole process of sending a message to the connected clients. - On line 35, we check that indeed our mock of
IHubCallerClients
was used and that it was only called once. - Line 37 is a bit more specific:
- Firstly, we’re checking for a call to
SendCoreAsync
, this is because the method we used in the hub called SendAsync
is actually an extension method that just wraps the parameters into an array and sends it to SendCoreAsync
. - We check that indeed the method that is to be called on the client side is actually named
welcome
. - We then check that the message that was sent is not
null
(being an and clause then it would short circuit if it is null
), that it has a Length
of 1 (remember from earlier that the messages get wrapped into an additional array) and that the first element in that collection is indeed an object array with 3 items (our messages). - We also have to provide the default for
CancelationToken
since Moq
can’t validate for optional parameters. - And lastly, we check that the message was sent only once.
And with that, we have now tested that our SignalR
hub is indeed working as intended. Using this approach, in a separate project, I could also fine grain test everything that was being passed in, including when the message is for only one specific client.
And that concludes our post for testing SignalR for ASP.NET Core 2.1.0.
Hope you enjoyed it and see you next time,
CodeProject