最近,我们将所有项目更新到 .NET 8,现在我们需要重新为 SignalR 进行单元测试,因为现在使用 Microsoft.Azure.Functions.Worker.SignalRService.ServerlessHub
我们的函数就是这样设置的
[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 方法在这里并不重要,因为我正在模拟 IAuthenticationManager,因为它是我们的。问题在于 NegotiateAsync,因为它是 ServerlessHub 的受保护方法。
有没有办法模拟ServerlessHub?我应该为它创建一个包装类,以便我可以进行直通吗?这里最好的解决方案是什么?
单元测试旨在测试您的应用程序逻辑。但是,SignalR 不是您的应用程序逻辑的一部分;它是您的应用程序逻辑的一部分。它只是数据进出的传输机制。
所以我会改变你的方法。将您的应用程序逻辑编写在不依赖于
ServerlessHub
的类中,这样您就根本不必模拟它。
您尚未提供有关应用程序逻辑如何与
ServerlessHub
交互(或分离)的任何信息。但这里有一个虚构的示例,它将您的协商逻辑移至可以进行单元测试的类中。
让我们创建一个具有调用身份验证管理器并生成一些声明的逻辑的类。
如果无法通过身份验证,它还会引发异常。
public class MyMessengerLogic
{
private readonly IAuthenticationManager _authMgr;
public MyMessengerLogic(IAuthenticationManager authMgr)
{
_authMgr = authMgr;
}
public async Task<NegotiationOptions> CreateNegotiationAsync(string userId)
{
MyUserEntity? user = await _authMgr.AuthenticateAsync(userId);
if (user == null) throw new MyAuthenticationException("Unauthenticated user");
return new()
{
UserId = user.UserId,
Claims = GenerateSomeClaims(user)
};
}
private Claims[] GenerateSomeClaims(MyUserEntity user)
{
// TODO...
}
}
[SignalRConnection("ConnectionStrings:SignalR")]
public class SignalRMessageProcessor : ServerlessHub
{
private readonly MyMessengerLogic _service;
public SignalRMessageProcessor(
IServiceProvider serviceProvider,
MyMessengerLogic service) : base(serviceProvider)
{
_service = service;
}
[Function("negotiate")]
public async Task<IActionResult> Negotiate(
[HttpTrigger("get", Route = "negotiate/{userId}")] HttpRequest req,
string userId)
{
try
{
// Anything that you want to unit test is now inside this service method:
var negotiationOptions = await _service.CreateNegotiationAsync(userId);
// From this point onwards you're just passing it to the Hub
// (this doesn't need unit testing)
var negotiateResponse = await NegotiateAsync(negotiationOptions);;
var response = JsonConvert.DeserializeObject<SignalRConnectionInfo>(negotiateResponse.ToString());
return new OkObjectResult(response);
}
catch (MyAuthenticationException aex)
{
// Catch the service exception if unauthenticated.
return new StatusCodeResult(StatusCodes.Status401Unauthorized);
}
}
}
现在所有可测试的逻辑都在一个单独的类中,您可以轻松地对其进行单元测试...而不必担心模拟
ServicelessHub
。
public class MessengerLogicUnitTests
{
[Fact]
public Task Negotiate_WithInvalidUser_ThrowsAuthException()
{
var authMgr = new Mock<IAuthenticationManager>();
authMgr
.Setup(f => f.AuthenticateAsync(It.IsAny<string>()))
.Returns(Task.FromResult(null));
var service = new MyMessengerLogic(authMgr.Object);
// Check an exception is thrown.
Assert.ThrowsAsync<MyAuthenticationException>(service.CreateNegotiationAsync("123"));
}
[Fact]
public async Task Negotiate_WithAuthenticatedUser_ReturnsNegotation()
{
var authMgr = new Mock<IAuthenticationManager>();
authMgr
.Setup(f => f.AuthenticateAsync("123"))
.Returns(Task.FromResult(new MyUserEntity("123", new Claim[] { /* etc */ })));
var service = new MyMessengerLogic(authMgr.Object);
var negotation = await service.CreateNegotiationAsync("123");
// Check our logic has returned some claims.
Assert.True(negotation.Claims.Any());
}
}