如何为包含ManualResetEvent.WaitOne()的异步(套接字)代码编写单元测试?

时间:2012-11-08 06:11:05

标签: c# .net unit-testing sockets tdd

我正在使用套接字服务器并尝试按照测试驱动开发模式工作。

套接字创建方法有一个工作测试,但我的测试因为我有一个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_IsAcceptingConnectionsAsynchronously()
{
    ITcpClient client = MockRepository.GenerateMock<ITcpClient>();
    ITcpListener listener = MockRepository.GenerateMock<ITcpListener>();
    IAsyncResult asyncResult = MockRepository.GenerateMock<IAsyncResult>();

    listener.Expect(x => x.BeginAcceptTcpClient(null, null)).IgnoreArguments().Return(asyncResult);
    listener.Expect(x => x.EndAcceptTcpClient(asyncResult)).Return(client);

    NetworkChannel networkChannel = new NetworkChannel(listener);

    networkChannel.StartListening();
    //Some more work... fake callback etc., verfify expectations,...
}

如果我评论所有的manualresetevents,测试通过就好了,但这不是解决方案,因为服务器试图连续创建重复的套接字; - )

对我有任何暗示吗?非常感谢!

此致 马丁

2 个答案:

答案 0 :(得分:0)

修改

这里的微妙之处在于StartListening方法会阻塞,所以我原来的方法不起作用,因为我们不能简单地等待它继续完成,但是在我们知道之前我们不能安全地继续NetworkChannel已经开始倾听。

解决方法是让频道在启动时提供一些通知。我可以想到几个选项:

NetworkChannel以下内容:

public delegate void OnStartupComplete();
public event OnStartupComplete StartupComplete;

StartListening之前的allDone.WaitOne();方法:

if (this.StartupComplete != null)
{
    StartupComplete();
}

通过以下方式设置活动:

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作为参数,并在Set之前调用其allDone.WaitOne();方法。您可以通过以下方式进行设置:

ManualResetEvent resetEvent = new ManualResetEvent(false);
NetworkChannel networkChannel = new NetworkChannel();
networkChannel.StartListening(resetEvent);

resetEvent.WaitOne();

答案 1 :(得分:0)

问题是,你想要准确测试什么?

如果测试只是希望方法StartListening调用listener.BeginAcceptTcpClient,那么最简单的解决方案就是在你的SUT中注入和模拟同步对象(ManualResetEvent),就像你做的那样非阻塞模拟的监听器。

如果要测试阻塞,则必须在调用listener.BeginAcceptTcpClient之前在测试中启动另一个线程,该线程在某些安全等待时间后发出ManualResetEvent信号 - 让我们说两秒钟 - 但这甚至可能还不够,所以你的测试结果可能不像Grzegorz评论那样具有确定性。

线程和单元测试可能会有些复杂。