我正在尝试使用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 - 或者至少是函数 - 并删除它。那就是说,这套方法
ReadAsync
并计算接收缓冲区ArraySegment
为阅读任务创建Task<int>
,并为IRemoteStream.ReadAsync(byte[],int,int)
的此任务添加延续,并将先前创建的_ReadAsyncContinuation
作为参数传递。ArraySegment
。正如我所提到的,此测试偶尔失败(MessageReceived
条件未得到满足)。我知道它与我的线程有关,但我不知道从哪里开始。想法?
对于布朗尼点:我知道我需要在这里给接收缓冲区添加独占写锁;我应该把它们放在哪里?