连续进行多个多线程测试时,测试 运行 停止
Test run stops when doing several multi-threaded tests in a row
我有一个带有静态 ConcurrentQueue 的 class。一个 class 接收消息并将它们放入队列,而此 class 上的另一个线程从该队列中读取它们并一次处理它们。该方法被取消标记中止。
清空队列的方法如下所示:
public async Task HandleEventsFromQueueAsync(CancellationToken ct, int pollDelay = 25)
{
while (true)
{
if (ct.IsCancellationRequested)
{
return;
}
if(messageQueue.TryDequeue(out ConsumeContext newMessage))
{
handler.Handle(newMessage);
}
try
{
await Task.Delay(pollDelay, ct).ConfigureAwait(true);
}
catch (TaskCanceledException)
{
return;
}
}
}
我的测试方法是这样的:
CancellationToken ct = source.Token;
Thread thread = new Thread(async () => await sut.HandleEventsFromQueueAsync(ct));
thread.Start();
EventListener.messageQueue.Enqueue(message1);
EventListener.messageQueue.Enqueue(message2);
await Task.Delay(1000);
source.Cancel(false);
mockedHandler.Verify(x => x.Handle(It.IsAny<ConsumeContext>()), Times.Exactly(2));
所以我在它自己的线程中启动我的出队方法,使用一个新的取消标记。然后我将几条消息放入队列,给进程一秒钟时间来处理它们,然后使用 source.Cancel(false) 结束线程并使方法成为 return。然后我检查处理程序被调用的次数是否正确。当然,当我中止出队方法时,我正在使用不同的消息类型和不同的时间来测试它的几个变体。
问题是,当我 运行 单独进行任何测试时,它们都会成功。但是当我尝试将它们 运行 作为一个组时,Visual Studio 并不是每个测试都 运行。没有错误消息,它所做的测试 运行 成功,但 运行 只是在第二次测试后停止。
我不知道为什么会这样。我的测试在结构上都是相同的。我每次都正确地中止出队线程。
什么可以迫使 Visual Studio 停止测试 运行,而不引发任何类型的错误?
我已经解决了我自己的问题。结果是新创建的线程抛出了一个异常,当线程抛出异常时,这些异常被忽略了,但它们仍然阻止了单元测试的发生。修复导致异常的问题后,测试工作正常。
您正在将异步 lambda 传递给 Thread
构造函数。 Thread
构造函数不理解异步委托(不接受 Func<Task>
参数),因此您最终得到一个 async void
lambda。 Async void methods should be avoided 用于任何不是事件处理程序的东西。在您的情况下,显式创建的线程在代码命中第一个 await
时终止,而主体的其余部分在 ThreadPool
线程中运行。似乎代码永远不会因异常而失败,否则进程会崩溃(这是 async void 方法的默认行为)。
建议:
- 使用
Task
而不是 Thread
。这样你在退出测试前就可以 await
了。
CancellationToken ct = source.Token;
Task consumerTask = Task.Run(() => sut.HandleEventsFromQueueAsync(ct));
EventListener.messageQueue.Enqueue(message1);
EventListener.messageQueue.Enqueue(message2);
await Task.Delay(1000);
source.Cancel(false);
await consumerTask; // Wait the task to complete
mockedHandler.Verify(x => x.Handle(It.IsAny<ConsumeContext>()), Times.Exactly(2));
考虑使用 BlockingCollection
or an asynchronous queue like a Channel
而不是 ConcurrentQueue
。轮询是一种笨拙且低效的技术。使用阻塞或异步队列,您将不必执行循环等待新消息到达。您将能够进入 waiting
状态,并在收到新消息时立即收到通知。
配置等待 ConfigureAwait(false)
。 ConfigureAwait(true)
是默认值,什么都不做。
考虑通过抛出 OperationCanceledException
来传播取消。这是 .NET 中传播取消的 standard way。所以代替:
if (ct.IsCancellationRequested) return;
...最好这样做:
ct.ThrowIfCancellationRequested();
我有一个带有静态 ConcurrentQueue 的 class。一个 class 接收消息并将它们放入队列,而此 class 上的另一个线程从该队列中读取它们并一次处理它们。该方法被取消标记中止。
清空队列的方法如下所示:
public async Task HandleEventsFromQueueAsync(CancellationToken ct, int pollDelay = 25)
{
while (true)
{
if (ct.IsCancellationRequested)
{
return;
}
if(messageQueue.TryDequeue(out ConsumeContext newMessage))
{
handler.Handle(newMessage);
}
try
{
await Task.Delay(pollDelay, ct).ConfigureAwait(true);
}
catch (TaskCanceledException)
{
return;
}
}
}
我的测试方法是这样的:
CancellationToken ct = source.Token;
Thread thread = new Thread(async () => await sut.HandleEventsFromQueueAsync(ct));
thread.Start();
EventListener.messageQueue.Enqueue(message1);
EventListener.messageQueue.Enqueue(message2);
await Task.Delay(1000);
source.Cancel(false);
mockedHandler.Verify(x => x.Handle(It.IsAny<ConsumeContext>()), Times.Exactly(2));
所以我在它自己的线程中启动我的出队方法,使用一个新的取消标记。然后我将几条消息放入队列,给进程一秒钟时间来处理它们,然后使用 source.Cancel(false) 结束线程并使方法成为 return。然后我检查处理程序被调用的次数是否正确。当然,当我中止出队方法时,我正在使用不同的消息类型和不同的时间来测试它的几个变体。
问题是,当我 运行 单独进行任何测试时,它们都会成功。但是当我尝试将它们 运行 作为一个组时,Visual Studio 并不是每个测试都 运行。没有错误消息,它所做的测试 运行 成功,但 运行 只是在第二次测试后停止。
我不知道为什么会这样。我的测试在结构上都是相同的。我每次都正确地中止出队线程。
什么可以迫使 Visual Studio 停止测试 运行,而不引发任何类型的错误?
我已经解决了我自己的问题。结果是新创建的线程抛出了一个异常,当线程抛出异常时,这些异常被忽略了,但它们仍然阻止了单元测试的发生。修复导致异常的问题后,测试工作正常。
您正在将异步 lambda 传递给 Thread
构造函数。 Thread
构造函数不理解异步委托(不接受 Func<Task>
参数),因此您最终得到一个 async void
lambda。 Async void methods should be avoided 用于任何不是事件处理程序的东西。在您的情况下,显式创建的线程在代码命中第一个 await
时终止,而主体的其余部分在 ThreadPool
线程中运行。似乎代码永远不会因异常而失败,否则进程会崩溃(这是 async void 方法的默认行为)。
建议:
- 使用
Task
而不是Thread
。这样你在退出测试前就可以await
了。
CancellationToken ct = source.Token;
Task consumerTask = Task.Run(() => sut.HandleEventsFromQueueAsync(ct));
EventListener.messageQueue.Enqueue(message1);
EventListener.messageQueue.Enqueue(message2);
await Task.Delay(1000);
source.Cancel(false);
await consumerTask; // Wait the task to complete
mockedHandler.Verify(x => x.Handle(It.IsAny<ConsumeContext>()), Times.Exactly(2));
考虑使用
BlockingCollection
or an asynchronous queue like aChannel
而不是ConcurrentQueue
。轮询是一种笨拙且低效的技术。使用阻塞或异步队列,您将不必执行循环等待新消息到达。您将能够进入waiting
状态,并在收到新消息时立即收到通知。配置等待
ConfigureAwait(false)
。ConfigureAwait(true)
是默认值,什么都不做。考虑通过抛出
OperationCanceledException
来传播取消。这是 .NET 中传播取消的 standard way。所以代替:
if (ct.IsCancellationRequested) return;
...最好这样做:
ct.ThrowIfCancellationRequested();