You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.16 10:40:35