【问题标题】:How to write unit test for asynchronous (socket) code that includes ManualResetEvent.WaitOne()?如何为包含 ManualResetEvent.WaitOne() 的异步(套接字)代码编写单元测试?
【发布时间】:2012-10-28 07:42:36
【问题描述】:

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

有一个套接字创建方法的工作测试,但我的测试挂起,因为我有一个 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,...
}

如果我评论所有的手动重置事件,测试通过就好了,但这不是解决方案,因为服务器会不断尝试创建重复的套接字;-)

对我有什么提示吗?将不胜感激!

问候, 马丁

【问题讨论】:

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


    【解决方案1】:

    编辑:

    这里的微妙之处在于 StartListening 方法阻塞,所以我原来的方法行不通,因为我们不能简单地等待它完成再继续,但是在我们知道 @ 之前我们不能安全地继续987654322@已开始收听。

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

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

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

    【讨论】:

    • 这不是以任何方式解决测试问题。我怀疑在单元测试中执行线程是个好主意。在您的方法中您不知道频道何时真正开始收听。所以测试结果不会是确定性的。
    • @GrzegorzWilczura 关于非确定性的好观点 - 我已经在我的编辑中解决了这个问题。
    • 也许我不明白这一点,但它如何帮助我在 WaitOne() 之前调用 ManualResetEvent.Set 方法?这使测试通过,但由于重复套接字问题,集成测试失败。在单元测试的单独线程中调用 StartListening() 并在单元测试中伪造连接是否有效?
    • 您是否只需要一个 NetworkChannel 来进行所有测试?请记住,我们调用 Set 的事件与 allDone 事件不同。我建议这样做的原因是为了解决 networkChannel.StartListening 方法阻塞并阻止后续代码运行的问题。
    • 尼克,感谢您的建议。现在我知道你是谁处理了这个问题。 StartupComplete 甚至在 NetworkChannel 中的 BeginAcceptTcpClient 调用之后立即触发。但这不会“激励”单元测试跳过“networkChannel.StartListening()”。我想我必须把它放到一个单独的线程中,这不是单元测试背后的意义。如何通过 resetEvent 传递侦听器并验证它是否指示“打开”状态?
    【解决方案2】:

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

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

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

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-03-26
      • 2020-04-15
      • 1970-01-01
      • 2013-03-18
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多