NSubstitute 在模拟子类上引发事件

NSubstitute raising an event on a mocked subclass

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

为了测试这个包装器 class 我已经模拟了 WebSocket class 并在那个 class 上做了 Raise.EventWith,我期待包装器 class 做 w/e 处理它应该。

代码是这样的:

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 被触发时,我从 class I正在测试。

我知道响应是 'bad design',但这对我帮助不大,你会如何解决这个问题?我需要包装 WebSocket class ,这不是我的,所以没有发言权。

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

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

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());
        }
    }
}

在测试中,您想要验证在 filesystemwatcher 的 10 个文件删除事件之后,您会从这个 class.

获得 "ItemsDeleted" 事件

认为 FileSystemWatcher 示例显示了我最挣扎的地方

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

为所需功能创建抽象

public interface IWebSocket {
    event EventHandler OnOpen;
    void ConnectAsync();

    //... other members
}

将该抽象显式注入目标 class

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());
    }
}

请注意目标 class 不再需要 expose/leak 实施问题。

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

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
}

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

最终的结果是,现在您的代码与外部依赖关系解耦,可以在不影响效果的情况下进行隔离测试,

[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 可以采用相同的方法。