如何解决SignalR单元测试中HubCallerContext.GetHttpContext无法模拟的问题?
SignalR Hub单元测试:Mock GetHttpContext()扩展方法的解决方案
问题描述
我在为SignalR Hub编写单元测试时,需要模拟Hub运行环境。在OnConnectedAsync()方法中,我通过请求头判断是否自动将连接加入分组,但遇到了障碍:HubCallerContext.GetHttpContext()是扩展方法,无法直接用Moq进行Mock。我尝试编写了测试基类代码,但未能解决该问题,需要可行的解决方案。
现有代码片段
OnConnectedAsync() 实现
Context.GetHttpContext().Request.Headers.TryGetValue(SignalRHeaders.GroupHeader, out StringValues header); if (header.Any()) { await Groups.AddToGroupAsync(Context.ConnectionId, SignalRConstants.Group); }
测试基类(原问题代码)
public DefaultHubBaseTest() { var memberId = Guid.NewGuid().ToString(); var orgId = Guid.NewGuid().ToString(); MockClients = new Mock<IHubCallerClients>(); MockClientProxy = new Mock<IClientProxy>(); MockClients.Setup(clients => clients.Group(It.IsAny<string>())) .Returns(MockClientProxy.Object); MockGroups = new Mock<IGroupManager>(); MockGroups.Setup(x => x.AddToGroupAsync(It.IsAny<string>(), It.IsAny<string>(), default(CancellationToken))).Returns(Task.CompletedTask); MockGroups.Setup(x => x.RemoveFromGroupAsync(It.IsAny<string>(), It.IsAny<string>(), default(CancellationToken))).Returns(Task.CompletedTask); Mock<HttpRequest> MockRequest = new Mock<HttpRequest>(); MockRequest.Setup(x => x.Headers).Returns(new HeaderDictionary() { { SignalRHeaders.GroupHeader, orgId }, { SignalRHeaders.GroupAdminHeader, "t" }, }); Mock<HttpContext> MockHttpContext = new Mock<HttpContext>(); MockHttpContext.Setup(x => x.Request).Returns(MockRequest.Object); MockContext = new Mock<HubCallerContext>(); MockContext.Setup(x => x.ConnectionId).Returns("1"); MockContext.Setup(x => x.User.Claims).Returns(new List<Claim>() { new Claim(SignalRConstants.AzureAuthOID, memberId) }); // 问题所在:直接Mock扩展方法不会生效 MockContext.Setup(x => x.GetHttpContext()).Returns(MockHttpContext.Object); DefaultHub = new DefaultHub() { Context = MockContext.Object, Groups = MockGroups.Object, Clients = MockClients.Object, }; }
解决方案
核心原理
GetHttpContext()是静态扩展方法,Moq无法直接Mock这类方法。它的底层逻辑是从HubCallerContext.Features集合中获取IHttpConnectionFeature实例,然后返回该实例的HttpContext属性。因此,我们需要MockIHttpConnectionFeature并将其注入到HubCallerContext的Features集合中,而非直接Mock扩展方法。
修正后的测试基类代码
public DefaultHubBaseTest() { var memberId = Guid.NewGuid().ToString(); var orgId = Guid.NewGuid().ToString(); MockClients = new Mock<IHubCallerClients>(); MockClientProxy = new Mock<IClientProxy>(); MockClients.Setup(clients => clients.Group(It.IsAny<string>())) .Returns(MockClientProxy.Object); MockGroups = new Mock<IGroupManager>(); MockGroups.Setup(x => x.AddToGroupAsync(It.IsAny<string>(), It.IsAny<string>(), default(CancellationToken))).Returns(Task.CompletedTask); MockGroups.Setup(x => x.RemoveFromGroupAsync(It.IsAny<string>(), It.IsAny<string>(), default(CancellationToken))).Returns(Task.CompletedTask); // 1. Mock HttpRequest 和 HttpContext Mock<HttpRequest> mockRequest = new Mock<HttpRequest>(); mockRequest.Setup(x => x.Headers).Returns(new HeaderDictionary() { { SignalRHeaders.GroupHeader, orgId }, { SignalRHeaders.GroupAdminHeader, "t" }, }); Mock<HttpContext> mockHttpContext = new Mock<HttpContext>(); mockHttpContext.Setup(x => x.Request).Returns(mockRequest.Object); // 2. Mock IHttpConnectionFeature,并关联到我们的HttpContext Mock<IHttpConnectionFeature> mockHttpConnectionFeature = new Mock<IHttpConnectionFeature>(); mockHttpConnectionFeature.Setup(f => f.HttpContext).Returns(mockHttpContext.Object); // 3. 创建FeatureCollection并注入IHttpConnectionFeature var featureCollection = new FeatureCollection(); featureCollection.Add(typeof(IHttpConnectionFeature), mockHttpConnectionFeature.Object); // 4. 配置HubCallerContext的Features,替代直接Mock扩展方法 MockContext = new Mock<HubCallerContext>(); MockContext.Setup(x => x.ConnectionId).Returns("1"); MockContext.Setup(x => x.User.Claims).Returns(new List<Claim>() { new Claim(SignalRConstants.AzureAuthOID, memberId) }); MockContext.Setup(x => x.Features).Returns(featureCollection); DefaultHub = new DefaultHub() { Context = MockContext.Object, Groups = MockGroups.Object, Clients = MockClients.Object, }; }
验证逻辑
运行测试时,Context.GetHttpContext()会自动从Features中获取我们预设的IHttpConnectionFeature,进而返回Mock的HttpContext,这样就能正确读取请求头并执行分组添加逻辑。
内容的提问来源于stack exchange,提问作者Nemean
相关产品推荐
相关产品推荐

