C# 如何为包含ManualResetEvent.WaitOne()的异步(套接字)代码编写单元测试?
我正在使用套接字服务器,并尝试按照测试驱动的开发模式工作 套接字创建方法有一个有效的测试,但是我的测试挂起,因为我有一个ManualResetEvent.WaitOne(),所以我的套接字在创建另一个连接之前等待连接 下面是要测试的代码块:C# 如何为包含ManualResetEvent.WaitOne()的异步(套接字)代码编写单元测试?,c#,.net,unit-testing,sockets,tdd,C#,.net,Unit Testing,Sockets,Tdd,我正在使用套接字服务器,并尝试按照测试驱动的开发模式工作 套接字创建方法有一个有效的测试,但是我的测试挂起,因为我有一个ManualResetEvent.WaitOne(),所以我的套接字在创建另一个连接之前等待连接 下面是要测试的代码块: ManualResetEvent allDone = new ManualResetEvent(false); public void StartListening() { allDone.Reset(); //Inform on the
ManualResetEvent allDone = new ManualResetEvent(false);
public void StartListening()
{
allDone.Reset();
//Inform on the console that the socket is ready
Console.WriteLine("Waiting for a connection...");
/* Waits for a connection and accepts it. AcceptCallback is called and the actual
* socket passed as AsyncResult
*/
listener.BeginAcceptTcpClient(
new AsyncCallback(AcceptCallback),
listener);
allDone.WaitOne();
}
下面是挂在“networkChannel.StartListening()上的测试方法
[TestMethod]
public void NetworkChannel_正在异步访问连接()
{
ITcpClient=MockRepository.GenerateMock();
ITcpListener listener=MockRepository.GenerateMock();
IAsyncResult asyncResult=MockRepository.GenerateMock();
Expect(x=>x.BeginAcceptTcpClient(null,null)).IgnoreArguments().Return(asyncResult);
Expect(x=>x.endAcceptCpcClient(asyncResult)).Return(客户端);
NetworkChannel NetworkChannel=新的NetworkChannel(侦听器);
networkChannel.StartListening();
//更多的工作…假回调等,非常期待,。。。
}
如果我对所有manualresetevents进行注释,测试通过得很好,但这不是解决方案,因为服务器试图连续创建重复的套接字;-)
有什么提示吗?非常感谢
问候,,
马丁编辑:
这里微妙的一点是,StartListening
方法会阻塞,因此我最初的方法不起作用,因为我们不能简单地等待它完成后再继续,但在知道NetworkChannel
已经开始监听之前,我们无法安全地继续
解决方法是让频道在启动时提供一些通知。我可以想出几个选项:
提供NetworkChannel
以下信息:
public delegate void OnStartupComplete();
public event OnStartupComplete StartupComplete;
在allDone.WaitOne();
前面的StartListening
方法中:
通过以下方式设置事件:
ManualResetEvent resetEvent = new ManualResetEvent(false);
NetworkChannel networkChannel = new NetworkChannel();
networkChannel.StartupComplete += () => { resetEvent.Set(); };
networkChannel.StartListening();
resetEvent.WaitOne();
编辑2:
这种方法(以及下面的方法)仍然存在一个问题,即阻塞StartListening
调用会阻止它后面的任何内容执行,比如调用resetEvent.WaitOne()
。我想你最好的办法是让StartListening
在另一个线程中运行它的代码。再加上StartupComplete
事件,我们应该得到我们想要的:网络频道在监听时通知我们,以及resetEvent
确保我们在听到之前不做任何事情
从单元测试的角度来看,使用另一个线程可能是不可取的,但我认为从设计的角度来看更可取:StartListening
当我们超出单元测试时,像它那样进行阻塞很可能会带来不便
为此,您可以在StartListening
方法中执行以下操作:
public void StartListening()
{
ThreadPool.QueueUserWorkItem((object state) =>
{
allDone.Reset();
//Inform on the console that the socket is ready
Console.WriteLine("Waiting for a connection...");
/* Waits for a connection and accepts it. AcceptCallback is called and the
* actual socket passed as AsyncResult
*/
listener.BeginAcceptTcpClient(new AsyncCallback(AcceptCallback), listener);
if (this.StartupComplete != null)
{
StartupComplete();
}
allDone.WaitOne();
});
}
或者,您可以简单地修改
StartListening
方法,将ManualResetEvent
作为参数,并在allDone.WaitOne();
之前调用其Set
方法
ManualResetEvent resetEvent = new ManualResetEvent(false);
NetworkChannel networkChannel = new NetworkChannel();
networkChannel.StartListening(resetEvent);
resetEvent.WaitOne();
问题是,你想测试什么 如果测试只希望方法
StartListening
调用listener.BeginAcceptTcpClient
,那么最简单的解决方案是在SUT中注入并模拟同步对象(ManualResetEvent
),就像使用非阻塞模拟对侦听器所做的那样
当您想要测试阻塞时,您必须在调用listener.beginacepttcpclient
之前启动测试中的另一个线程,该线程会在一段安全等待时间(比如说两秒钟)后发出ManualResetEvent信号,但这可能还不够,因此您的测试结果可能不像Grzegorz所评论的那样具有确定性
线程和单元测试可能会变得有些复杂。这并不是解决任何测试问题。我怀疑在单元测试中执行线程是一个好主意。在您的方法中,您不知道通道何时真正开始侦听。因此测试结果将不具有确定性。@GrzegorzWilczura关于非确定性的一个好观点-我在我的编辑中已经解决了这个问题。也许我没有抓住要点,但它如何帮助我在WaitOne()之前调用ManualResetEvent.Set方法?这会使测试通过,但由于重复套接字问题,集成测试失败。调用StartListening()是否有效在单元测试中的单独线程中,并从单元测试中伪造连接?您是否希望所有测试只使用一个NetworkChannel?请记住,我们正在调用的Set事件与allDone事件不同。我建议这样做的原因是为了避免NetworkChannel.StartListening方法阻塞的问题并阻止后续代码运行。Nick,谢谢你的建议。现在我知道你是如何处理这个问题的了。StartupComplete甚至在NetworkChannel中调用BeginAcceptTcpClient后立即启动。但这并没有“激励”单元测试跳过“NetworkChannel.StartListening()“我想我必须把它放到一个单独的线程中,这不是单元测试背后的意义。如何通过resetEvent传递侦听器,并验证它是否指示“打开”状态?
public void StartListening()
{
ThreadPool.QueueUserWorkItem((object state) =>
{
allDone.Reset();
//Inform on the console that the socket is ready
Console.WriteLine("Waiting for a connection...");
/* Waits for a connection and accepts it. AcceptCallback is called and the
* actual socket passed as AsyncResult
*/
listener.BeginAcceptTcpClient(new AsyncCallback(AcceptCallback), listener);
if (this.StartupComplete != null)
{
StartupComplete();
}
allDone.WaitOne();
});
}
ManualResetEvent resetEvent = new ManualResetEvent(false);
NetworkChannel networkChannel = new NetworkChannel();
networkChannel.StartListening(resetEvent);
resetEvent.WaitOne();