Thursday, 12 July 2012

Unit Testing SignalR Hubs

SignalR is an awesome library for building realtime async applications using .Net. If you've not had a chance to use it yet then you should definately check out this SignalR article from Hanselman and find something to use it in immediately. You're missing out on all the fun!!

So all the cool kids are using SignalR, but I haven't seen many people talking about how to unit test a SignalR hub. In this post i'm going to explain how to set up a testable Hub and then write a sample unit test for it.

I would recommend keeping SignalR Hubs as skinny as possible, and moving as much logic as you can into seperate classes and services. However there will probably be times when you will need to write tests against a hub class, in the same way that we need to write tests against an asp.net mvc Controller.

Lets imagine we have a very simple chat application. In our application, when a message is sent by a client, the message is saved to a data store, and then broadcast to all clients.

This scenario is trivial with SignalR

public class ChatHub : Hub
    {
        private readonly IChatRepository _chatRepository;

        public ChatHub(IChatRepository chatRepository)
        {
            _chatRepository = chatRepository;
        }

        public bool Send(string message)
        {
            _chatRepository.SaveMessage(message, Context.ConnectionId);
            Clients.addMessage(message);
            return true;
        }
    }

In this simple example, i want to test that when a message is sent, the SaveMessage method is invoked on _chatRepository with the correct parameters and that the Send method returns true.

In order to test this we need to create a testable instance of ChatHub.

If we try to new up a ChatHub instance in a unit test, we'll get null reference exceptions from both Context.ConnectionId, and Clients.addMessage when Send is called.

We need to wire up the Context and Clients properties of the Hub.
It will also be useful to wire up the Caller property as this will likely be used at some point.

I'm using Moq in this example to mock the various different objects required to create a testable hub.

 public class TestableChatHub : ChatHub  
   {  
     public Mock<IChatRepository> MockChatRepository { get; private set; }
  
     public TestableChatHub(Mock<IChatRepository> mockChatRepository)  
       : base(mockChatRepository.Object)  
     {
       const string connectionId = "1234";  
       const string hubName = "Chat";  
       var mockConnection = new Mock<IConnection>();  
       var mockUser = new Mock<IPrincipal>();  
       var mockCookies = new Mock<IRequestCookieCollection>();
  
       var mockRequest = new Mock<IRequest>();  
       mockRequest.Setup(r => r.User).Returns(mockUser.Object);  
       mockRequest.Setup(r => r.Cookies).Returns(mockCookies.Object);
         
       Clients = new ClientAgent(mockConnection.Object, hubName);  
       Context = new HubCallerContext(mockRequest.Object, connectionId);
  
       var trackingDictionary = new TrackingDictionary();  
       Caller = new StatefulSignalAgent(mockConnection.Object, connectionId, hubName, trackingDictionary);  
     }  
   } 

Once the Context, Clients and Caller properties have been set on the Hub, writing tests against it is simple.

 using Moq;  
 using NUnit.Framework;  
 using SignalRTestableHubExample.Models;
  
 namespace SignalRTestableHubExample.Tests  
 {   
   [TestFixture]  
   public class ChatHubTests  
   {  
     private TestableChatHub _hub;
  
     [SetUp]  
     public void SetUpTests()  
     {  
       _hub = GetTestableChatHub();  
     }
  
     [Test]  
     public void ExampleTest()  
     {  
       const string message = "test";  
       const string connectionId = "1234";
  
       var result = _hub.Send(message);
  
       _hub.MockChatRepository.Verify(r => r.SaveMessage(message, connectionId));
       Assert.IsTrue(result);  
     }
  
     private TestableChatHub GetTestableChatHub()  
     {  
       var mockRepository = new Mock<IChatRepository>();  
       mockRepository.Setup(m => m.SaveMessage(It.IsAny<string>(), It.IsAny<string>())).Returns(true);  
       return new TestableChatHub(mockRepository);  
     }  
   }  
 }

No comments:

Post a Comment