使用Moq和xUnit测试异步方法看似不是线程安全的?

时间:2014-05-20 08:55:30

标签: c# unit-testing asynchronous moq

我正在尝试使用Moq和xUnit测试异步方法。测试在大多数情况下运行良好,但偶尔会失败。最初,我认为这是由于我的方法中的代码存在缺陷,直到我意识到如果它同步运行,这不会导致问题。另外,如果在另一个测试中Moq类上有替代设置,那么测试会失败更多。

这是一个问题,因为它意味着我无法可靠地测试我的类,因为它使用来自流的异步读取作为类核心的一部分。

以下是测试。

    [Fact]
    public void FullMessageAsynchronouslyRead_RaiseMessageReceivedEvent()
    {
        var stream = new Mock<IRemoteStream>();
        var schema = new Mock<IMessageSerializationSchema>();
        var remote = new TCPRemote(stream.Object, schema.Object) as IRemote;
        var mockListener = new Mock<ITCPRemoteTestEventListener>();
        var message = Mock.Of<IMessage>();
        // Use manual reset event to ensure that we don't finish the test
        // until the async method has returned
        var reset = new ManualResetEvent(false);
        schema.Setup(x => x.Read(It.IsAny<byte[]>())).Returns(message);
        remote.MessageReceived += mockListener.Object.MessageReceived;
        stream.Setup(x => x.ReadAsync(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()))
            .ReturnsAsync(1)
            .Callback(() => reset.Set());

        remote.ReadAsync();

        reset.WaitOne(TimeSpan.FromMilliseconds(100));
        mockListener.Verify(x => x.MessageReceived(remote, message), Times.Once());
    }

此测试应测试当我收到完整消息(通过检查IMessageSerializationSchema.Read(byte[])是否引发异常而确定)时,MessageReceived事件被引发TCPRemote。为了测试它是否已被引发,我创建了一个名为ITCPRemoteTestEventListener的测试接口并对该接口进行了模拟,将其作为事件处理程序分配给TCPRemote.MessageReceived,然后使用Moq验证它是否接到对它的调用回调恰好一次。我使用ManualResetEvent尝试使测试保持一定的同步,否则测试可能总是在较慢的系统上失败(因为读操作是异步的)。

以下是SUT ReadAsync方法中涉及的代码。

    public void ReadAsync()
    {
        Array.Clear(_receiveBuffer, 0, 0);
        _ReadAsync(_receiveBuffer, 0, 0);
    }

    #region Internals
    private void _ReadAsync(byte[] buffer, int offset, int count)
    {
        var segment = new ArraySegment<byte>(buffer, offset, count);
        _stream.ReadAsync(buffer, offset, count).ContinueWith(_ReadAsyncContinuation, segment);
    }

    private void _ReadAsyncContinuation(Task<int> readAsyncTask, object segment)
    {
        var seg = (ArraySegment<byte>)segment;
        var bytes = new byte[seg.Count];
        Array.Copy(_receiveBuffer, seg.Offset, bytes, 0, seg.Count);

        try
        {
            var message = _schema.Read(bytes);

            if (MessageReceived != null)
                MessageReceived(this, message);
        }
        catch (SerializedMessageNotFullyReceivedException)
        {
            _ReadAsync(_receiveBuffer, seg.Offset, ReceiveBufferSize - seg.Offset);
        }
    }
    #endregion

长话短说,这最初写的是它可以自动循环(调用ReadAsync会调用_ReadAsync,然后当_ReadAsync读完一条消息时,它会调用{{1再次,清除缓冲区并重新开始)但是我决定打破类的SRP - 或者至少是函数 - 并删除它。那就是说,这套方法

  • 清除接收缓冲区(8024字节)
  • 根据偏移量创建ReadAsync并计算接收缓冲区
  • 通过调用ArraySegment为阅读任务创建Task<int>,并为IRemoteStream.ReadAsync(byte[],int,int)的此任务添加延续,并将先前创建的_ReadAsyncContinuation作为参数传递。
  • 通过询问Schema来检查消息是否有效 - 如果是,则Schema将只返回消息。
  • 将活动发布到ArraySegment

正如我所提到的,此测试偶尔失败(MessageReceived条件未得到满足)。我知道它与我的线程有关,但我不知道从哪里开始。想法?

对于布朗尼点:我知道我需要在这里给接收缓冲区添加独占写锁;我应该把它们放在哪里?

0 个答案:

没有答案