C# 如何为包含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.WaitOne(),所以我的套接字在创建另一个连接之前等待连接

下面是要测试的代码块:

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