升级.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
相关产品推荐
相关产品推荐

