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
可以采用相同的方法。
我拥有的是来自外部库(本例为 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
可以采用相同的方法。