【问题标题】:NSubstitute raising an event on a mocked subclassNSubstitute 在模拟的子类上引发事件
【发布时间】:2019-02-17 09:33:18
【问题描述】:

我拥有的是来自外部库(本例是 WebSocketSharp)的类的包装器,包装器类对某些事件做出反应,例如建立连接时等。

为了测试这个包装类,我模拟了 WebSocket 类并在该类上做了 Raise.EventWith,我希望 Wrapper 类能够处理它应该做的事情。

代码是这样的:

public class WebsocketClient {
    public event EventHandler Connected;

    public WebSocket Connection { get;set; }

    public void ConnectAsync() {
        Connection.OnOpen += Connection_OnOpen;
        Connection.ConnectAsync();
    }

    private void Connection_OnOpen(object sender, System.EventArgs e) {
        Connected?.Invoke(this, new EventArgs());
    }
}

我想写的测试是:

public void ConnectedTest() {
    var objClient = new WebsocketClient();
    var raised = false;
    objClient.Connected += delegate (object sender, EventArgs e) {
        raised = true;
    }; 
    var objWebSocket = Substitute.For<WebSocketSharp.WebSocket>("wss://localhost:443");
    objClient.Connection = objWebSocket;
    objClient.ConnectAsync();
    objClient.Connection.OnOpen += Raise.EventWith(new object(), new EventArgs());

    objWebSocket.Received().OnOpen += Arg.Any<EventHandler>();
    Assert.IsTrue(raised);
}

显然,事情被简化了一点,因为还有一些事情需要检查,这只是为了让大家了解这个想法。

在测试中,我想验证两件事,即当调用 ConnectAsync 时,会将事件处理程序添加到 OnOpen 事件中,并且当触发 OnOpen 时,我会从正在测试的类中返回一个事件。

我知道的反应是,这是“糟糕的设计”,但这对我没有多大帮助,你将如何解决这个问题?,我需要包装 WebSocket 类,这不是我的所以有没有发言权。

在这种情况下,我唯一能想到的是扩展 WebSocket 类而不是制作包装器,但我确信我需要进行其他测试,其中扩展不是一种选择,并且编写这样的包装器会需要,例如,当使用事件处理程序是私有的文件系统观察器或计时器时,仍想测试事件被触发时会发生什么。

为了演示,例如如何测试这个? (没有跑任何东西,只是为了展示想法)

public class FilesDeleted {
    private FileSystemWatcher _objWatcher;
    private List<string> _lstPaths;

    public event EventHandler ItemsDeleted;

    public FilesDeleted(string pPath) {
        _lstPaths = new List<string>();
        _objWatcher = new FileSystemWatcher(pPath);
    }

    public void Start() {
        _objWatcher.Deleted += _objWatcher_Deleted;
        _objWatcher.EnableRaisingEvents = true;
    }

    private void _objWatcher_Deleted(object sender, FileSystemEventArgs e) {
        _lstPaths.Add(e.FullPath); 
        if(_lstPaths.Count > 10) {
            ItemsDeleted?.Invoke(this, new EventArgs());
        }
    }
}

在测试中,您想要验证在文件系统观察器中的 10 个文件删除事件之后,您会从此类中获得“ItemsDeleted”事件。

认为 FileSystemWatcher 示例显示了我最苦恼的问题

【问题讨论】:

    标签: c# testing mocking nunit nsubstitute


    【解决方案1】:

    您关于不良设计的陈述是准确的。这里的问题是您与无法控制的第三部分实现问题紧密耦合,这使得孤立地测试您的代码变得困难。

    为所需功能创建抽象

    public interface IWebSocket {
        event EventHandler OnOpen;
        void ConnectAsync();
    
        //... other members
    }
    

    将该抽象显式注入目标类

    public class WebsocketClient {
        private readonly IWebSocket connection;
    
        public WebsocketClient(IWebSocket connection) {
            this.connection = connection;
        }
    
        public event EventHandler Connected = delegate { };
    
        public void ConnectAsync() {
            connection.OnOpen += Connection_OnOpen;
            connection.ConnectAsync();
        }
    
        private void Connection_OnOpen(object sender, System.EventArgs e) {
            Connected.Invoke(this, new EventArgs());
        }
    }
    

    请注意,目标类也不再需要暴露/泄露实现问题。

    在生产代码中,抽象 Web 套接字的实现将包装实际的 3rd 方依赖项。这是将在运行时注入依赖类的内容。

    public class DefaultWebSocketWrapper : IWebSocket {
        private WebSocket webSocket;
    
        public DefaultWebSocketWrapper() {
            webSocket = new WebSocket("wss://localhost:443");
        }
    
        public event EventHandler OnOpen {
            add {
                webSocket.OnOpen += value;
            }
            remove {
                webSocket.OnOpen -= value;
            }
        }
    
        public void ConnectAsync() {
            webSocket.ConnectAsync();
        }
    
        //... other members
    }
    

    这个类不需要测试,因为它只是一个你无法控制的外部代码的包装器。因此测试它会浪费时间。

    最终的结果是,现在你的代码与外部依赖解耦了,可以独立测试,不会产生连锁反应,

    [TestClass]
    public class WebSocketTests {
        [Test]
        public void ConnectedTest() {
            //Arrange
            var webSocketMock = Substitute.For<IWebSocket>();
    
            var subject = new WebsocketClient(webSocketMock);
            bool raised = false;
            subject.Connected += delegate(object sender, EventArgs e) {
                raised = true;
            };
            subject.ConnectAsync();
    
            //Act
            webSocketMock.OnOpen += Raise.Event();
    
            //Assert
            Assert.IsTrue(raised);
        }
    }
    

    以上安全测试WebsocketClient,无需担心外部第 3 方依赖项,因为您可以控制所有正在使用的代码。

    您的问题中描述的FileSystemWatcher 可以采用相同的方法。

    【讨论】:

    • 由于您的回答,我能够开始并且知道我必须了解更多关于依赖注入的信息(是的,谷歌),对于阅读本文的任何人,这是一个很好的起点youtube.com/watch?v=UiOC1jsQI1o
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-05-17
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多