Click here to Skip to main content
15,885,164 members
Articles / Web Development / ASP.NET / ASP.NET Core

Testing SignalR Hubs in ASP.NET Core 2.1

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
12 Nov 2018CPOL4 min read 12.1K   3   1
How to test SignalR Hubs in ASP.NET Core 2.1

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:

C#
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:

C#
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:

JavaScript
@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.

C#
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()
        {
            // arrange
            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
            };

            // act
            await simpleHub.Welcome();


            // assert
            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:

  1. 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.
  2. 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
  3. We then create a mock for both IHubCallerClients and IClientProxy
  4. 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.
  5. We then create a SimpleHub and tell it to use our mock for its Clients delegation. Now we have full control over the flow.
  6. We do the call to SimpleHub.Welcome which starts the whole process of sending a message to the connected clients.
  7. On line 35, we check that indeed our mock of IHubCallerClients was used and that it was only called once.
  8. 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,

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Romania Romania
When asked, I always see myself as a .Net Developer because of my affinity for the Microsoft platform, though I do pride myself by constantly learning new languages, paradigms, methodologies, and topics. I try to learn as much as I can from a wide breadth of topics from automation to mobile platforms, from gaming technologies to application security.

If there is one thing I wish to impart, that that is this "Always respect your craft, your tests and your QA"

Comments and Discussions

 
QuestionCannot Verify SendCoreAsync(string, default(CancellationToken) Pin
felipedr1-Feb-19 19:35
felipedr1-Feb-19 19:35 
Hi Vlad,

I changed the Welcome method to just send a welcome message, without the array.

Clients.All.SendAsync("welcome");

But when it comes to testing, clientProxy.SendCoreAsync has only 1 method signature, which is
(string method, object[] args, CancellationToken cancellationToken = default(CancellationToken))

mockClientProxy.Verify(
clientProxy => clientProxy.SendCoreAsync(
"welcome",
It.Is<object[]>(o => o != null && o.Length == 1 && ((object[])o[0]).Length == 3),
default(CancellationToken)),
Times.Once);

So that I cannot test it! Why is that?!

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.