【问题标题】:How to mock a wrapped Microsoft ASP.NET Core Client SignalR HubConnection class?如何模拟包装的 Microsoft ASP.NET Core 客户端 SignalR HubConnection 类?
【发布时间】:2020-11-25 17:00:16
【问题描述】:

我正在实现我的第一个 signalR 应用程序,但在测试方面遇到了绊脚石。我正在使用Moq 框架。

不幸的是,我发现微软的HubConnection 类使用Moq 框架不容易模拟,即它没有虚拟方法,也没有直接实现接口。微软官方的suggestion是给开发者实现一个包装类和接口的,我已经做过了,见下面类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());
        }

【问题讨论】:

    标签: c# asp.net-core moq asp.net-core-signalr


    【解决方案1】:

    创建包装类背后的整个想法是模拟包装类。包装类仅充当您尝试使用的实际类的传递,因此它不会有任何自己的逻辑。由于您使用的是 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")
    

    【讨论】:

    • 谢谢@dogyear。是的,谢谢,那部分是有道理的。这是否意味着我不为 hubproxy 本身编写测试,即检查代理是否通过实际类的测试?集线器代理的代码覆盖率如何?
    • @anon_dcs3spp 您为代理编写测试,而不是实际的类。我添加了另一个示例。您对测试感兴趣的是返回结果,因此您可以模拟您的 HubProxy 以返回您正在测试的结果。
    • 好的,我想我明白了,谢谢 :) 通过模拟代理,覆盖范围仍然会受到影响,对吧?
    • 我不确定您所说的“覆盖范围仍然受到打击”是什么意思,但是因为代理只是一个传递(实际上是实际 HubConnection 的代理),所以您实际上是在嘲笑 HubConnection。所以任何需要模拟 HubConnection 的测试都可以模拟 IHubProxy。
    • 谢谢!不,我不相信它会击中那个代码。您很可能需要为 HubProxy 的具体实现编写一个集成测试,该测试将使用实际的 HubConnection 来获得 HubProxy 的测试覆盖率。 IHubProxy 接口对单元测试所做的只是允许接口的使用者使用从它返回的“值”,因为您正在为您的测试提供您自己的 IHubProxy 实现(模拟)而不是 IHubProxy 的实际实现(作为 HubProxy)。
    猜你喜欢
    • 1970-01-01
    • 2017-11-18
    • 1970-01-01
    • 2020-04-27
    • 2016-01-13
    • 2019-08-25
    • 2018-06-20
    • 1970-01-01
    • 2012-10-10
    相关资源
    最近更新 更多