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

升级.NET 8后,Azure Function中ServerlessHub的单元测试方案咨询

.NET8下ServerlessHub的SignalR单元测试解决方案

我们近期将所有项目升级至.NET 8,当前使用Microsoft.Azure.Functions.Worker.SignalRService.ServerlessHub,需要重新编写SignalR相关单元测试。现有函数结构如下:

[SignalRConnection("ConnectionStrings:SignalR")]
public class SignalRMessageProcessor : ServerlessHub
{
    private readonly IAuthenticationManager _authenticationManager;
    private readonly ILogger<SignalRMessageProcessor> _logger;

    public SignalRMessageProcessor(
        IServiceProvider serviceProvider,
        IAuthenticationManager authenticationManager,
        ILogger<SignalRMessageProcessor> logger) : base(serviceProvider)
    {
        _authenticationManager = authenticationManager;
        _logger = logger;
    }

    [Function("negotiate")]
    public async Task<IActionResult> Negotiate(
        [HttpTrigger("get", Route = "negotiate/{userId}")] HttpRequest req,
        string userId)
    {
        var statusCode = Authenticate(req);
        if (statusCode != StatusCodes.Status200OK)
        {
            return new StatusCodeResult(statusCode);
        }
        _logger.LogInformation($"userId: {userId}; Negotiate Function triggered");

        var negotiateResponse = await NegotiateAsync(new() { UserId = userId });
        
        var response = JsonConvert.DeserializeObject<SignalRConnectionInfo>(negotiateResponse.ToString());

        return new OkObjectResult(response);
    }
}

其中Authenticate方法已通过Mock IAuthenticationManager处理,但NegotiateAsync是ServerlessHub的protected方法,无法直接Mock。针对以下问题给出解决方案:

  • 是否可以Mock ServerlessHub?
  • 是否需要创建包装类做透传?
  • 最优解决方案是什么?

解决方案

1. 直接Mock Protected方法(无需额外包装)

可以用Moq的Protected()API直接MockServerlessHub中的NegotiateAsync方法,不需要修改业务代码,成本最低。示例代码:

// 构建SignalRMessageProcessor的Mock实例
var mockProcessor = new Mock<SignalRMessageProcessor>(
    Mock.Of<IServiceProvider>(),
    Mock.Of<IAuthenticationManager>(),
    Mock.Of<ILogger<SignalRMessageProcessor>>())
{
    CallBase = false // 禁用基类真实逻辑调用
};

// Mock protected的NegotiateAsync方法
mockProcessor.Protected()
             .Setup<Task<object>>(
                 "NegotiateAsync",
                 ItExpr.IsAny<NegotiationOptions>())
             .ReturnsAsync(new
             {
                 Url = "https://mock-signalr-endpoint",
                 AccessToken = "mock-token-123"
             });

// 执行测试方法
var result = await mockProcessor.Object.Negotiate(Mock.Of<HttpRequest>(), "test-user-1");

// 断言结果
var okResult = Assert.IsType<OkObjectResult>(result);
var connectionInfo = Assert.IsType<SignalRConnectionInfo>(okResult.Value);
Assert.Equal("https://mock-signalr-endpoint", connectionInfo.Url);

2. 抽象层封装(长期可维护性优先)

如果担心直接Mock protected方法不够直观,或未来ServerlessHubAPI变更风险,可以封装抽象层解耦:

步骤1:定义抽象接口

public interface ISignalRNegotiationService
{
    Task<object> NegotiateAsync(NegotiationOptions options);
}

步骤2:实现封装类

public class SignalRNegotiationService : ISignalRNegotiationService
{
    private readonly ServerlessHub _hub;

    public SignalRNegotiationService(ServerlessHub hub)
    {
        _hub = hub;
    }

    public async Task<object> NegotiateAsync(NegotiationOptions options)
    {
        return await _hub.NegotiateAsync(options);
    }
}

步骤3:修改业务类

将NegotiateAsync的调用委托给封装服务:

public class SignalRMessageProcessor : ServerlessHub
{
    private readonly IAuthenticationManager _authenticationManager;
    private readonly ILogger<SignalRMessageProcessor> _logger;
    private readonly ISignalRNegotiationService _negotiationService;

    public SignalRMessageProcessor(
        IServiceProvider serviceProvider,
        IAuthenticationManager authenticationManager,
        ILogger<SignalRMessageProcessor> logger,
        ISignalRNegotiationService negotiationService) : base(serviceProvider)
    {
        _authenticationManager = authenticationManager;
        _logger = logger;
        _negotiationService = negotiationService;
    }

    [Function("negotiate")]
    public async Task<IActionResult> Negotiate(
        [HttpTrigger("get", Route = "negotiate/{userId}")] HttpRequest req,
        string userId)
    {
        var statusCode = Authenticate(req);
        if (statusCode != StatusCodes.Status200OK)
        {
            return new StatusCodeResult(statusCode);
        }
        _logger.LogInformation($"userId: {userId}; Negotiate Function triggered");

        var negotiateResponse = await _negotiationService.NegotiateAsync(new() { UserId = userId });
        
        var response = JsonConvert.DeserializeObject<SignalRConnectionInfo>(negotiateResponse.ToString());

        return new OkObjectResult(response);
    }
}

步骤4:单元测试

直接Mock抽象接口即可:

var mockNegotiationService = new Mock<ISignalRNegotiationService>();
mockNegotiationService.Setup(s => s.NegotiateAsync(It.IsAny<NegotiationOptions>()))
                      .ReturnsAsync(new
                      {
                          Url = "https://mock-signalr-endpoint",
                          AccessToken = "mock-token-123"
                      });

var processor = new SignalRMessageProcessor(
    Mock.Of<IServiceProvider>(),
    Mock.Of<IAuthenticationManager>(),
    Mock.Of<ILogger<SignalRMessageProcessor>>(),
    mockNegotiationService.Object);

var result = await processor.Negotiate(Mock.Of<HttpRequest>(), "test-user-1");

// 断言逻辑...

3. 最优方案选择

  • 快速临时解决:直接Mock protected方法,无需修改业务代码,实现成本最低。
  • 长期可维护:抽象层封装,将业务逻辑与ServerlessHub实现解耦,便于后续扩展和测试,适合大型或需要长期迭代的项目。

内容的提问来源于stack exchange,提问作者Javier Buffa

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 23:12:08