测试一个多线程应用程序的单元测试?

33

有没有关于一致性地对多线程应用进行单元测试的建议?我曾经做过一个应用程序,其中我们的模拟“工作线程”使用了Thread.Sleep方法,并指定了一个公共成员变量来设置每个线程执行其工作所需的时间。我们使用这种方法来设置特定线程完成其工作所需的时间,然后进行断言。还有更好的方法吗?有没有适用于.Net的良好模拟框架可以处理这个问题?


3
可能是 如何对多线程代码进行单元测试? 的重复问题。 - Warren Dew
9个回答

31

第一步是认识到通常大部分需要进行单元测试的代码与线程无关。这意味着你应该将负责工作的代码和负责线程的代码分开。完成此操作后,您可以使用常规的单元测试实践轻松测试执行工作的代码。但是,当然,您已经知道了这一点。

问题在于测试问题的线程部分,但至少现在您有一个接口与处理工作的代码相连,并且希望您在那里有一个可以模拟的接口。现在您对于调用线程代码的代码已经有了一个模拟,我发现最好的做法是向模拟添加一些事件(这可能意味着您需要手动编写模拟)。事件将用于允许测试与正在测试的线程代码同步和阻止。

例如,假设我们有一个非常简单的多线程队列来处理工作项。您将对工作项进行模拟。接口可能包含一个“Process()”方法,该线程调用该方法来执行工作。您会在其中放置两个事件。当调用Process()时,模拟设置一个事件;在设置第一个事件后,模拟等待另一个事件。现在,在您的测试中,您可以启动队列,发布一个模拟工作项,然后等待工作项的“正在被处理”事件。如果您只测试调用过程是否正确,那么可以设置另一个事件并让线程继续。如果您正在测试其他更复杂的内容,例如队列如何处理多个调度等,则可能在释放线程之前执行其他操作(例如发布和等待其他工作项)。由于您可以在测试中使用超时进行等待,因此可以确保只有两个工作项以并行方式进行处理。关键是使用线程代码块的事件使测试变得确定性,以便测试可以控制它们运行的时间。

我相信您的情况更为复杂,但这是我用来测试并发代码的基本方法,它效果非常好。如果您模拟了正确的部分并放置了同步点,那么您可以对多线程代码获得惊人的控制力。

以下是关于这种事情的更多信息,尽管它是关于C++代码库的: http://www.lenholgate.com/blog/2004/05/practical-testing.html


24

我的建议是不要依赖单元测试来检测并发问题,原因如下:

  • 缺乏可重复性:测试仅在某些情况下失败,并不真正有助于准确定位问题。
  • 不稳定的失败构建会让团队中的每个人都感到烦恼 - 因为最后一次提交总是被错误地怀疑是导致构建失败的原因。
  • 遇到死锁时可能会冻结构建,直到执行超时被触发,这可能会显著减慢构建速度。
  • 构建环境可能是单 CPU 环境(比如在虚拟机中运行构建),并发问题可能永远不会发生 - 无论设置多少睡眠时间。
  • 这样做有点违背了代码验证的简单、隔离的“单元”的理念。

3
小建议:有时候编写一个单元测试来证明竞态条件/死锁是可行的。如果您在修复代码之前(或者所有者修复代码之前)就能够做到这一点,那么您已经帮助解释了问题并确保修复措施足够有效。否则同意。 - Oskar
12
我不同意,但我已经成功地使用单元测试一段时间来测试并发代码了。关键在于从测试中控制被测试的代码 - 请参阅我对原始问题的回复。我认为通过尝试使用单元测试尽可能多地进行测试,你可以学到很多关于你的代码所面临的并发问题。这很困难,并且让你深思熟虑地考虑被测试的代码和并发性问题,而这是一个好事,我个人认为。 - Len Holgate
5
虽然这些建议可能是正确的,但它们只是告诉我们"不应该做什么",并没有提供具体要怎么做。建议依靠确定性的单元测试(不能有线程定时或多组件依赖)和彻底的多组件回归测试。Len Holgate的答案为并发问题单元测试提供了一个很好的模板。 - David Jeske

11

如果你需要测试一个后台线程是否执行了某个操作,我通常会使用一种简单的技术,即编写一个名为 WaitUntilTrue 的方法,大致如下:

bool WaitUntilTrue(Func<bool> func,
              int timeoutInMillis,
              int timeBetweenChecksMillis)
{
    Stopwatch stopwatch = Stopwatch.StartNew();

    while(stopwatch.ElapsedMilliseconds < timeoutInMillis)
    {
        if (func())
            return true;
        Thread.Sleep(timeBetweenChecksMillis);
    }   
    return false;
}

用法如下:

volatile bool backgroundThreadHasFinished = false;
//run your multithreaded test and make sure the thread sets the above variable.

Assert.IsTrue(WaitUntilTrue(x => backgroundThreadHasFinished, 1000, 10));

这样,您就不必让主要的测试线程长时间休眠以便让后台线程有足够的时间来完成工作。如果后台线程在合理的时间内没有完成工作,则测试将失败。


2
在我看来,使用事件进行同步和等待要比像那样旋转更好;但原则上我同意。 - Len Holgate
1
@Len:这种方法的优点是通常很容易编写一个小函数来确定正在测试的代码是否已完成,但您不希望修改代码以发出仅由测试使用的特殊信号量。 - Matt Howells
我的大部分代码已经为生产使用设置了监控钩子,这些钩子通常很好地提供了测试可以插入模拟并等待的点。如果您不是这样设计的话,那么我猜旋转比不测试要好;) - Len Holgate
请查看MSDN博客上的这篇文章:使用.NET进行并行编程[SpinWait.SpinUntil用于单元测试](http://blogs.msdn.com/b/pfxteam/archive/2011/02/15/10129633.aspx)。我投票支持SpinWait.SpinUntil。 - Volker von Einem

7

1
这就是我对TypeMock的期望,例如使做错事情变得容易,因为有时候做错事情比什么都不做更好。 - Ian Ringrose

4

我发现了一个名为Microsoft Chess的研究产品。它专门用于非确定性测试多线程应用程序。目前的缺点是它只能集成到VS中。


是的,看起来它正在工作... 唯一的缺点是,它只适用于C++和C# - inf3rno

2
我认为单元测试不是发现线程错误的有效方法,但它们可以是演示已知线程错误、隔离错误并测试其修复的良好方式。我还使用它们来测试应用程序中某些协调类的基本功能,例如阻塞队列。

我将multithreadedTC库从Java移植到.NET,并将其命名为TickingTest。它允许您从单元测试方法启动多个线程并协调它们。它没有原始库的所有功能,但我发现它很有用。最大的缺陷是无法监视在测试期间启动的线程的能力。


2

在多处理器机器上测试多线程代码非常重要。双核机器可能不足够。我曾经看到在双核单处理器上没有发生的死锁在4个处理器的机器上发生了。然后,您需要创建一个基于客户端程序的压力测试,该程序生成许多线程并对目标应用程序进行多个请求。如果客户端机器也是多处理器的,则有更多的负载会落在目标应用程序上。


0

虽然不完全是单元测试,但您可以编写一些测试代码,反复调用将在不同线程上执行的代码。尝试在线程之间创建最大交错,并进行周期性或最终一致性检查。当然,这种方法的缺点是无法重现,因此您需要使用广泛的日志记录来找出问题所在。最好将此方法与每个线程各自任务的单元测试相结合。


0

像GUI单元测试一样,这对于自动化测试来说是一个难点。真正的线程本质上是不可预测的,它们会以无法预先确定的方式干扰。因此,编写真正的测试是困难的,如果不是不可能的话。

然而,你并不孤单...我建议搜索一下testdrivendevelopment Yahoo群组的存档。我记得有一些相关的帖子.. 这是其中较新的一个。 (如果有人能够友好地进行概述和解释,那就太好了。我太困了...需要休息)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接