我正在实现我的第一个 signalR 应用程序,并且在测试方面遇到了障碍。我正在使用
Moq
框架。
不幸的是,我发现 Microsoft 的 HubConnection 类不容易使用
Moq
框架进行模拟,即它没有虚拟方法,并且不直接实现接口。微软官方的建议是让开发者实现一个包装类和接口,我已经做到了,请参阅下面的类HubProxy
的小示例。我现在遇到的问题是如何测试被包装的HubConnection
类是被包装器触发的?
我最初从 HubConnection 派生了一个类,下面的
MockedHubConnection
使用 new
来尝试和重写。 MockedHubConnection
方法会抛出 NotImplementedException
。在下面的测试中,它断言抛出 NotImplementedException 来表示已触发包装的 HubConnection。但是,我在测试中收到 NullReference 异常:
Actual: typeof(System.NullReferenceException): Object reference not set to an instance of an object.
---- System.NullReferenceException : Object reference not set to an instance of an object.
Stack Trace:
at Microsoft.Extensions.Logging.Logger`1.Microsoft.Extensions.Logging.ILogger.BeginScope[TState](TState state)
at Microsoft.AspNetCore.SignalR.Client.HubConnection.StopAsync(CancellationToken cancellationToken)
----- Inner Stack Trace -----
at Microsoft.Extensions.Logging.Logger`1.Microsoft.Extensions.Logging.ILogger.BeginScope[TState](TState state)
at Microsoft.AspNetCore.SignalR.Client.HubConnection.StopAsync(CancellationToken cancellationToken)
我认为正在发生的事情是,
HubProxy
类的构造函数隐式地将模拟HubConnection转换为真实的HubConnection
实例??
如何测试包装第三方类的包装类,而该第三方类不提供可模拟接口或可重写的虚拟方法?
Hub 代理示例 - 包裹 HubConnection
public class HubProxy : IHubProxy
{
private readonly HubConnection connection;
public HubProxy(HubConnection connection)
{
this.connection = connection;
}
public Task StopAsync(CancellationToken cancel=default)
{
this.connection.StopAsync(cancel);
}
}
模拟 HubConnection - 抛出 NotImplementedException 来测试由包装器调用
public class MockHubConnection : HubConnection
{
public MockHubConnection(
IConnectionFactory connection,
IHubProtocol protocol,
EndPoint endPoint,
IServiceProvider provider,
ILoggerFactory factory,
IRetryPolicy policy
) : base(connection, protocol, endPoint, provider, factory, policy) { }
public new Task StopAsync(CancellationToken token = default)
{
throw new NotImplementedException();
}
}
模拟 HubConnection - 初始尝试 - 使用 new 来覆盖
public class MockHubConnection : HubConnection
{
public MockHubConnection(
IConnectionFactory connection,
IHubProtocol protocol,
EndPoint endPoint,
IServiceProvider provider,
ILoggerFactory factory,
IRetryPolicy policy
) : base(connection, protocol, endPoint, provider, factory, policy) { }
public new Task StopAsync(CancellationToken token = default)
{
throw new NotImplementedException();
}
}
测试样品
[Fact]
public async Task HubProxy_StopAsync_Invokes_HubConnection_StopAsync()
{
var mockCon = Mock.Of<IConnectionFactory>();
var mockHubProtocol = Mock.Of<IHubProtocol>();
var mockServiceProvider = Mock.Of<IServiceProvider>();
var mockLoggerFactory = Mock.Of<ILoggerFactory>();
var mockRetryPolicy = Mock.Of<IRetryPolicy>();
var mockHubConnection = new MockHubConnection(
mockCon,
mockHubProtocol,
new IPEndPoint(0, 0),
mockServiceProvider,
mockLoggerFactory,
mockRetryPolicy
);
var _client = new HubProxy(mockHubConnection);
await Assert.ThrowsAsync<NotImplementedException>(() => _client.StopAsync());
}
创建包装类背后的整个想法是模拟包装类。包装类仅充当您尝试使用的实际类的传递,因此它自己不具有任何逻辑。由于您使用的是 MOQ,因此无需为
HubConnection
创建模拟类。您会嘲笑 IHubProxy
,这样您就不会使用与 HubConnection
相关的任何内容。
var mockHubProxy = new Mock<IHubProxy>();
// Since you're only trying find out if a
// method is called then you would use MOQ's verify method.
mockHubProxy.Verify(x => x.StopAsync());
// If you really want an exception to be thrown then you
// would do the following. Though I don't think this is the way to go.
mockHubProxy.Setup(x => x.StopAsync()).Throws(new NotImplementedException());
这有道理吗?请注意,这里没有关于
HubConnection
的内容。包装器 HubProxy
的全部目的是删除与 HubConnection
相关的任何内容,因为它无法进行测试。在 HubProxy
的实际实现中,您的 StopAsync
方法将调用 HubConnection.StopAsync()
的具体实现。在你的模拟中它什么也不做。它只是确保它被调用。
我不知道
HubConnection
的方法,因此我将在下面的示例中使用虚构的方法名称来说明如何为实际具有返回值的方法实现包装器。
public interface IHubProxy
{
string GetStringValue();
}
public HubProxy: IHubProxy
{
private readonly HubConnection _connection;
public HubProxy()
{
_connection = new HubConnection();
}
public string GetStringValue()
{
return _connection.GetStringValue();
}
}
你的模拟将如下所示
var mockHubProxy = new Mock<IHubProxy>();
mockHubProxy.Setup(x => x.GetStringValue()).Returns("expected string value")
使用 Wrapper(例如 IHubProxy)仍然是模拟 HubConnection 的最佳方法吗? (我对这个解决方案并不是很感兴趣,因为它意味着更多的维护。)