我该如何对线程代码进行unit testing?

到目前为止,我已经避免了测试multithreading代码的噩梦,因为它看起来像是一个雷区太多了。 我想问一下人们如何测试依赖线程成功执行的代码,或者人们如何测试那些只在两个线程以给定方式交互时出现的问题?

对于今天的程序员来说,这似乎是一个非常关键的问题,将我们的知识集中在这个imho上是有用的。

看,没有简单的方法可以做到这一点。 我正在开发一个本质上是multithreading的项目。 事件来自操作系统,我必须同时处理它们。

处理复杂的multithreading应用程序代码的最简单方法是:如果它太复杂而无法测试,那么你做错了。 如果您有一个具有多个线程的单个实例,并且您无法测试这些线程相互重叠的情况,那么您的设计需要重做。 它既简单又复杂。

有许多方法可以为multithreading编程,避免线程同时运行在实例中。 最简单的方法是使所有对象都不可变。 当然,这通常是不可能的。 因此,您必须在设计中识别线程与同一实例交互并减少这些位置数量的位置。 通过这样做,您可以隔离实际发生multithreading的几个类,从而降低测试系统的整体复杂性。

但是你必须意识到,即使通过这样做,你仍然无法测试两个线程相互衔接的每种情况。 要做到这一点,你必须在同一个测试中同时运行两个线程,然后在任何给定时刻准确控制它们正在执行的行。 你能做的最好的就是模拟这种情况。 但这可能需要您专门为测试编写代码,而这最多只是迈向真正解决方案的一半。

测试线程问题代码的最佳方法可能是通过代码的静态分析。 如果您的线程代码不遵循一组有限的线程安全模式,那么您可能会遇到问题。 我相信VS中的Code Analysis确实包含一些线程知识,但可能并不多。

看看,目前的情况(并且可能会有好的时机),测试multithreading应用程序的最佳方法是尽可能降低线程代码的复杂性。 最小化线程交互的区域,尽可能测试,并使用代码分析来识别危险区域。

这个问题发布时已经有一段时间了,但仍然没有回答……

kleolb02的答案很好。 我会尝试进入更多细节。

有一种方法,我为C#代码练习。 对于unit testing,您应该能够编写可重现的测试,这是multithreading代码中的最大挑战。 所以我的答案旨在将异步代码强制转换为同步工作的测试工具。

这是Gerard Meszardos的书“ xUnit Test Patterns ”中的一个想法,被称为“Humble Object”(第695页):你必须将核心逻辑代码和任何闻起来像异步代码的东西分开。 这将导致核心逻辑的类,它同步工作

这使您能够以同步方式测试核心逻辑代码。 您可以完全控制对核心逻辑进行调用的时间,从而可以进行可重复的测试。 这是分离核心逻辑和异步逻辑的好处。

这个核心逻辑需要由另一个类包围,该类负责异步接收对核心逻辑的调用, 并将这些调用委托给核心逻辑。 生产代码只能通过该类访问核心逻辑。 因为这个类只应该委托调用,所以它是一个非常“愚蠢”的类,没有太多的逻辑。 因此,您可以至少对这个非同步工作级别进行unit testing。

高于此值(测试类之间的交互)是组件测试。 同样在这种情况下,如果您坚持使用“Humble Object”模式,您应该能够对时间进行绝对控制。

确实很难! 在我的(C ++)unit testing中,我将其分解为使用的并发模式的几个类别:

  1. 对在单个线程中运行且不是线程感知的类的unit testing – 容易,像往常一样进行测试。

  2. Monitor对象 (在调用者的控制线程中执行同步方法的对象)的unit testing,它们公开同步的公共API – 实例化多个运行API的模拟线程。 构建行使被动对象内部条件的场景。 包括一个长时间运行的测试,它基本上可以在很长一段时间内从多个线程中击败它。 我知道这是不科学的,但它确实建立了信心。

  3. Active对象的unit testing(封装自己的线程或控制线程的对象) – 类似于上面的#2,其变化取决于类设计。 公共API可能是阻塞的或非阻塞的,呼叫者可能获得期货,数据可能到达队列或需要出列。 这里有许多可能的组合; 白盒子走了。 仍然需要多个模拟线程来调用被测对象。

作为旁白:

在我做的内部开发人员培训中,我教授并发支柱和这两种模式作为思考和分解并发问题的主要框架。 显然有更先进的概念,但我发现这套基础知识有助于让工程师摆脱困境。 如上所述,它还导致代码更加可unit testing。

近几年来,我为几个项目编写线程处理代码时,我多次遇到过这个问题。 我提供了一个迟到的答案,因为大多数其他答案在提供替代方案时,实际上并未回答有关测试的问题。 我的答案是针对没有multithreading代码替代的情况; 我确实涵盖了代码设计问题的完整性,但也讨论了unit testing。

编写可测试的multithreading代码

首先要做的是将生产线程处理代码与执行实际数据处理的所有代码分开。 这样,数据处理可以作为单线程代码进行测试,multithreading代码唯一能做的就是协调线程。

要记住的第二件事是multithreading代码中的错误是概率性的; 最不经常表现出来的错误是潜入生产的错误,即使在生产中也难以复制,因此会造成最大的问题。 出于这个原因,快速编写代码然后调试它直到它工作的标准编码方法对于multithreading代码来说是一个坏主意; 它将导致代码中的容易错误被修复,危险的错误仍然存​​在。

相反,在编写multithreading代码时,您必须以一种态度编写代码,以避免首先编写错误。 如果你已经正确删除了数据处理代码,那么线程处理代码应该足够小 – 最好是几行,最差几十行 – 你有机会编写它而不会编写bug,当然也没有写出很多bug ,如果你理解穿线,花点时间,并且小心。

编写unit testingmultithreading代码

一旦尽可能仔细地编写multithreading代码,仍然值得为该代码编写测试。 测试的主要目的不是测试高度时序相关的竞争条件错误 – 不可能重复测试这种竞争条件 – 而是测试您的防止此类错误的锁定策略允许多个线程按预期进行交互。

要正确测试正确的锁定行为,测试必须启动多个线程。 为了使测试可重复,我们希望线程之间的交互以可预测的顺序发生。 我们不希望外部同步测试中的线程,因为这将掩盖在线程未进行外部同步的生产中可能发生的错误。 这就留下了线程同步的时序延迟,这是我在编写multithreading代码测试时成功使用的技术。

如果延迟太短,则测试变得脆弱,因为较小的时序差异 – 例如在可能运行测试的不同机器之间 – 可能导致时序关闭并且测试失败。 我通常做的是从导致测试失败的延迟开始,增加延迟以便测试在我的开发机器上可靠地通过,然后将延迟加倍,以便测试很有可能传递到其他机器上。 这确实意味着测试需要花费大量的时间,但根据我的经验,仔细的测试设计可以将时间限制在不超过十几秒。 由于您的应用程序中不应该有很多需要线程协调代码的地方,因此您的测试套件应该可以接受。

最后,跟踪测试中捕获的错误数量。 如果您的测试具有80%的代码覆盖率,则可以预计会捕获大约80%的错误。 如果您的测试设计合理但没有发现错误,那么您有可能没有其他只会出现在生产中的错误。 如果测试中遇到一个或两个错误,您可能仍然很幸运。 除此之外,您可能需要仔细审查甚至完全重写您的线程处理代码,因为代码可能仍然包含在代码生产之前很难找到的隐藏错误,并且非常那时很难解决。

我在测试multithreading代码时也遇到了严重的问题。 然后我在Gerard Meszaros的“xUnit Test Patterns”中找到了一个非常酷的解决方案。 他描述的模式称为Humble对象

基本上它描述了如何将逻辑提取到一个与环境分离的独立,易于测试的组件中。 在测试了这个逻辑之后,您可以测试复杂的行为(multithreading,异步执行等……)

有一些工具非常好。 以下是一些Java的摘要。

一些好的静态分析工具包括FindBugs (提供一些有用的提示), JLint , Java Pathfinder (JPF和JPF2)和Bogor 。

MultithreadedTC是一个非常好的动态分析工具(集成到JUnit中),您必须在其中设置自己的测试用例。

IBM Research的ConTest非常有趣。 它通过插入各种线程修改行为(例如睡眠和产量)来检测代码,以尝试随机发现错误。

SPIN是一个非常酷的工具,用于建模Java(和其他)组件,但您需要有一些有用的框架。 它很难按原样使用,但如果你知道如何使用它会非常强大。 相当多的工具在引擎盖下使用SPIN。

multithreadingTC可能是最主流的,但上面列出的一些静态分析工具绝对值得一看。

我做了很多这样的事情,是的很糟糕。

一些技巧:

  • GroboUtils用于运行多个测试线程
  • alphaWorks ConTest用于检测类,以使迭代之间的交错变化
  • 创建一个throwable字段并在tearDown检查它(参见清单1)。 如果你在另一个线程中捕获到一个错误的exception,只需将它分配给throwable。
  • 我在清单2中创建了utils类,并发现它非常有价值,特别是waitForVerify和waitForCondition,这将大大提高测试的性能。
  • 在测试中充分利用AtomicBoolean 。 它是线程安全的,你经常需要一个最终的引用类型来存储来自回调类等的值。 请参阅清单3中的示例。
  • 确保始终为测试提供超时(例如,@ Test @Test(timeout=60*1000) ),因为并发测试有时会在它们被破坏时永远挂起

清单1:

 @After public void tearDown() { if ( throwable != null ) throw throwable; } 

清单2:

 import static org.junit.Assert.fail; import java.io.File; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.Random; import org.apache.commons.collections.Closure; import org.apache.commons.collections.Predicate; import org.apache.commons.lang.time.StopWatch; import org.easymock.EasyMock; import org.easymock.classextension.internal.ClassExtensionHelper; import static org.easymock.classextension.EasyMock.*; import ca.digitalrapids.io.DRFileUtils; /** * Various utilities for testing */ public abstract class DRTestUtils { static private Random random = new Random(); /** Calls {@link #waitForCondition(Integer, Integer, Predicate, String)} with * default max wait and check period values. */ static public void waitForCondition(Predicate predicate, String errorMessage) throws Throwable { waitForCondition(null, null, predicate, errorMessage); } /** Blocks until a condition is true, throwing an {@link AssertionError} if * it does not become true during a given max time. * @param maxWait_ms max time to wait for true condition. Optional; defaults * to 30 * 1000 ms (30 seconds). * @param checkPeriod_ms period at which to try the condition. Optional; defaults * to 100 ms. * @param predicate the condition * @param errorMessage message use in the {@link AssertionError} * @throws Throwable on {@link AssertionError} or any other exception/error */ static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, Predicate predicate, String errorMessage) throws Throwable { waitForCondition(maxWait_ms, checkPeriod_ms, predicate, new Closure() { public void execute(Object errorMessage) { fail((String)errorMessage); } }, errorMessage); } /** Blocks until a condition is true, running a closure if * it does not become true during a given max time. * @param maxWait_ms max time to wait for true condition. Optional; defaults * to 30 * 1000 ms (30 seconds). * @param checkPeriod_ms period at which to try the condition. Optional; defaults * to 100 ms. * @param predicate the condition * @param closure closure to run * @param argument argument for closure * @throws Throwable on {@link AssertionError} or any other exception/error */ static public void waitForCondition(Integer maxWait_ms, Integer checkPeriod_ms, Predicate predicate, Closure closure, Object argument) throws Throwable { if ( maxWait_ms == null ) maxWait_ms = 30 * 1000; if ( checkPeriod_ms == null ) checkPeriod_ms = 100; StopWatch stopWatch = new StopWatch(); stopWatch.start(); while ( !predicate.evaluate(null) ) { Thread.sleep(checkPeriod_ms); if ( stopWatch.getTime() > maxWait_ms ) { closure.execute(argument); } } } /** Calls {@link #waitForVerify(Integer, Object)} with null * for {@code maxWait_ms} */ static public void waitForVerify(Object easyMockProxy) throws Throwable { waitForVerify(null, easyMockProxy); } /** Repeatedly calls {@link EasyMock#verify(Object[])} until it succeeds, or a * max wait time has elapsed. * @param maxWait_ms Max wait time. null defaults to 30s. * @param easyMockProxy Proxy to call verify on * @throws Throwable */ static public void waitForVerify(Integer maxWait_ms, Object easyMockProxy) throws Throwable { if ( maxWait_ms == null ) maxWait_ms = 30 * 1000; StopWatch stopWatch = new StopWatch(); stopWatch.start(); for(;;) { try { verify(easyMockProxy); break; } catch (AssertionError e) { if ( stopWatch.getTime() > maxWait_ms ) throw e; Thread.sleep(100); } } } /** Returns a path to a directory in the temp dir with the name of the given * class. This is useful for temporary test files. * @param aClass test class for which to create dir * @return the path */ static public String getTestDirPathForTestClass(Object object) { String filename = object instanceof Class ? ((Class)object).getName() : object.getClass().getName(); return DRFileUtils.getTempDir() + File.separator + filename; } static public byte[] createRandomByteArray(int bytesLength) { byte[] sourceBytes = new byte[bytesLength]; random.nextBytes(sourceBytes); return sourceBytes; } /** Returns true if the given object is an EasyMock mock object */ static public boolean isEasyMockMock(Object object) { try { InvocationHandler invocationHandler = Proxy .getInvocationHandler(object); return invocationHandler.getClass().getName().contains("easymock"); } catch (IllegalArgumentException e) { return false; } } } 

清单3:

 @Test public void testSomething() { final AtomicBoolean called = new AtomicBoolean(false); subject.setCallback(new SomeCallback() { public void callback(Object arg) { // check arg here called.set(true); } }); subject.run(); assertTrue(called.get()); } 

Awaitility也可以帮助您编写确定性unit testing。 它允许您等到系统中的某个状态更新。 例如:

 await().untilCall( to(myService).myMethod(), greaterThan(3) ); 

要么

 await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1)); 

它还具有Scala和Groovy支持。

 await until { something() > 4 } // Scala example 

正如已经说明的那样,测试MT代码的正确性是一个非常难的问题。 最后,它归结为确保代码中没有错误同步的数据争用。 这样做的问题在于,你有很多可能的线程执行(交错),你没有太多的控制权(但请务必阅读本文)。 在简单的情况下,可能通过推理实际certificate正确性,但通常情况并非如此。 特别是如果你想避免/最小化同步而不是去寻找最明显/最简单的同步选项。

我遵循的方法是编写高度并发的测试代码,以便可能发生可能未检测到的数据争用。 然后我运行那些测试一段时间:)我曾经偶然发现了一个谈话,其中一些计算机科学家展示了一种工具,这种方式(从规范中随机设计测试,然后同时运行它们,同时检查定义的不变量)被打破)。

顺便说一句,我认为这里没有提到测试MT代码的方面:识别可以随机检查的代码的不变量。 不幸的是,找到那些不变量也是一个很难的问题。 此外,他们可能无法在执行期间保持所有时间,因此您必须找到/执行执行点,您可以预期它们是真实的。 将代码执行带到这样的状态也是一个难题(并且本身可能会引发并发问题。哇,这太难了!

一些有趣的链接:

  • 确定性交织 :允许强制某些线程交错然后检查不变量的框架
  • jMock Blitzer :压力测试同步
  • assertConcurrent :JUnit版本的压力测试同步
  • 测试并发代码 :对暴力(压力测试)或确定性(针对不变量)的两种主要方法的简要概述

另一种(有点)测试线程代码的方法,以及非常复杂的系统通常是通过Fuzz测试 。 它不是很好,它不会找到所有东西,但它可能很有用而且很容易做到。

引用:

模糊测试或模糊测试是一种软件测试技术,可为程序的输入提供随机数据(“模糊”)。 如果程序失败(例如,通过崩溃或内置代码断言失败),则可以注意到缺陷。 模糊测试的巨大优势在于测试设计非常简单,并且没有对系统行为的偏见。

模糊测试通常用于采用黑盒测试的大型软件开发项目中。 这些项目通常有预算来开发测试工具,而模糊测试是提供高成本效益的技术之一。

然而,模糊测试不能代替详尽的测试或正式方法:它只能提供系统行为的随机样本,并且在许多情况下通过模糊测试可能只能certificate一个软件处理exception而不会崩溃,而不是行为正确。 因此,模糊测试只能被视为一种错误查找工具,而不是质量保证。

Pete Goodliffe有一系列关于线程代码的unit testing 。

这个很难(硬。 我采取了更简单的方法,并尝试保持从实际测试中抽象出来的线程代码。 皮特确实提到我这样做的方式是错误的,但我要么正确分离,要么就是幸运。

对于Java,请查看JCIP的第12章。 编写确定性,multithreadingunit testing以至少测试并发代码的正确性和不变量有一些具体的例子。

“通过unit testingcertificate”螺纹安全性更加严格。 我相信,通过各种平台/配置的自动化集成测试可以更好地满足这一需求。

我处理线程组件的unit testing与处理任何unit testing的方式相同,即控制和隔离框架的反转。 我在.Net-arena中开发并开箱即用,线程(以及其他事情)很难(我说几乎不可能)完全隔离。

因此我写了一些看起来像这样(简化)的包装器:

 public interface IThread { void Start(); ... } public class ThreadWrapper : IThread { private readonly Thread _thread; public ThreadWrapper(ThreadStart threadStart) { _thread = new Thread(threadStart); } public Start() { _thread.Start(); } } public interface IThreadingManager { IThread CreateThread(ThreadStart threadStart); } public class ThreadingManager : IThreadingManager { public IThread CreateThread(ThreadStart threadStart) { return new ThreadWrapper(threadStart) } } 

从那里我可以轻松地将IThreadingManager注入到我的组件中,并使用我选择的隔离框架使线程在测试期间表现得如我所期望的那样。

到目前为止,这对我来说非常有用,我使用相同的方法来处理线程池,System.Environment,Sleep等等。

我喜欢编写两个或多个测试方法来在并行线程上执行,并且每个测试方法都会调用被测对象。 我一直在使用Sleep()调用来协调来自不同线程的调用顺序,但这并不可靠。 它也慢很多,因为你必须睡得足够长,以至于时间通常有效。

我从编写FindBugs的同一组中找到了multithreadingTC Java库 。 它允许您在不使用Sleep()的情况下指定事件的顺序,并且它是可靠的。 我还没有尝试过。

这种方法的最大限制是它只允许您测试您怀疑会造成麻烦的场景。 正如其他人所说的,你真的需要将你的multithreading代码分离成少量的简单类,以便彻底测试它们。

一旦你仔细测试了你希望引起麻烦的场景,一个不科学的测试会在课堂上抛出一堆同时发出的请求,这是寻找意外麻烦的好方法。

更新:我使用multithreadingTC Java库玩了一下,效果很好。 我还将其部分function移植到我称之为TickingTest的.NET版本中。

看看我的相关答案

为自定义屏障设计测试类

它偏向于Java,但对选项有一个合理的总结。

总而言之,虽然(IMO)它不是使用一些确保正确性的花式框架,而是如何设计multithreading代码。 分散关注点(并发性和function性)对提高信心起着重要作用。 以测试为导向的不断增长的以面向对象的软件解释了一些比我更好的选择。

静态分析和forms化方法(参见, 并发:状态模型和Java程序 )是一种选择,但我发现它们在商业开发中的用途有限。

不要忘记任何加载/浸泡样式测试很少能保证突出问题。

祝你好运!

我刚刚发现(用于Java)一个名为Threadsafe的工具。 它是一个静态分析工具,很像findbugs,但专门用于发现multithreading问题。 它不是测试的替代品,但我可以推荐它作为编写可靠的multithreadingJava的一部分。

它甚至可以捕获一些非常微妙的潜在问题,例如类包含,通过并发类访问不安全对象以及在使用双重检查锁定范例时发现丢失的volatile修饰符。

如果你编写multithreadingJava ,请试一试。

以下文章提出了两种解决方案。 包装信号量(CountDownLatch)并添加内部线程外部化数据等function。 实现此目的的另一种方法是使用线程池(请参阅兴趣点)。

Sprinkler – 高级同步对象

我上周大部分时间都在大学图书馆学习调试并发代码。 核心问题是并发代码是非确定性的。 通常情况下,学术调试已经落入三大阵营之一:

  1. 事件跟踪/重播。 这需要事件监视器,然后查看已发送的事件。 在UT框架中,这将涉及手动发送事件作为测试的一部分,然后进行事后审查。
  2. 编写脚本。 这是您使用一组触发器与正在运行的代码进行交互的地方。 “在x> foo,baz()”。 这可以解释为UT框架,其中您有一个运行时系统在特定条件下触发给定的测试。
  3. 互动。 这显然不适用于自动测试情况。 ;)

现在,正如上面的评论员所注意到的,您可以将并发系统设计为更具确定性的状态。 但是,如果你没有正确地做到这一点,你只需要再次设计一个顺序系统。

我的建议是专注于有一个非常严格的设计协议,关于什么是线程和什么没有线程。 如果限制界面以使元素之间的依赖性最小,则更容易。

祝你好运,并继续努力解决这个问题。

我有测试线程代码的不幸任务,它们绝对是我写过的最艰难的测试。

在编写测试时,我使用了委托和事件的组合。 基本上,所有关于将PropertyNotifyChanged事件与WaitCallback或某种轮询的ConditionalWaiter使用。

我不确定这是否是最好的方法,但它对我有用。

对于J2E代码,我使用了SilkPerformer,LoadRunner和JMeter来进行线程的并发测试。 他们都做同样的事情。 基本上,它们为您提供了一个相对简单的界面,用于管理其所需的代理服务器版本,以便分析TCP / IP数据流,并模拟多个用户同时向您的应用服务器发出请求。 代理服务器可以在处理请求后,通过显示发送到服务器的整个页面和URL以及服务器的响应,分析所做的请求。

您可以在不安全的http模式下找到一些错误,您可以在其中至少分析正在发送的表单数据,并系统地为每个用户更改该错误。 但真正的测试是在https(安全套接字层)中运行。 然后,您还必须应对系统地更改会话和cookie数据,这可能会更复杂一些。

在测试并发性时,我发现的最好的错误是当我发现开发人员依赖Java垃圾收集来关闭登录时建立的连接请求,登录时到LDAP服务器。这导致用户被暴露对于其他用户的会话和非常混乱的结果,当试图分析当服务器被瘫痪时发生的事情,几乎不能每隔几秒完成一次事务。

最后,你或某人可能不得不扣掉并分析代码,就像我刚才提到的那样。 当我们展开上述问题时,跨部门的公开讨论是最有用的。 但这些工具是测试multithreading代码的最佳解决方案。 JMeter是开源的。 SilkPerformer和LoadRunner是专有的。 如果你真的想知道你的应用程序是否是线程安全的,大男孩就是这样做的。 我已经为非常大的公司专业做过这个,所以我不猜。 我是根据个人经验说的。

需要注意的是:理解这些工具确实需要一些时间。 这不是简单地安装软件和启动GUI的问题,除非您已经接触过multithreading编程。 我已经尝试确定要理解的3个关键领域类别(表单,会话和cookie数据),希望至少从了解这些主题开始将有助于您专注于快速结果,而不是必须通读整个文档。

并发是内存模型,硬件,缓存和代码之间复杂的相互作用。 在Java的情况下,至少这样的测试部分主要由jcstress解决 。 众所周知,该库的创建者是许多JVM,GC和Java并发function的作者。

但即使是这个库也需要熟悉Java内存模型规范,以便我们确切地知道我们正在测试什么。 但我认为这项工作的重点是mircobenchmarks。 不是巨大的商业应用。

如果你正在测试简单的新线程(runnable).run()那么你可以模拟Thread来顺序运行runnable

例如,如果测试对象的代码调用这样的新线程

 Class TestedClass { public void doAsychOp() { new Thread(new myRunnable()).start(); } } 

然后模拟新的Threads并顺序运行runnable参数可以提供帮助

 @Mock private Thread threadMock; @Test public void myTest() throws Exception { PowerMockito.mockStatic(Thread.class); //when new thread is created execute runnable immediately PowerMockito.whenNew(Thread.class).withAnyArguments().then(new Answer() { @Override public Thread answer(InvocationOnMock invocation) throws Throwable { // immediately run the runnable Runnable runnable = invocation.getArgumentAt(0, Runnable.class); if(runnable != null) { runnable.run(); } return threadMock;//return a mock so Thread.start() will do nothing } }); TestedClass testcls = new TestedClass() testcls.doAsychOp(); //will invoke myRunnable.run in current thread //.... check expected } 

(如果可能)不使用线程,使用actor / active对象。 易于测试。

您可以使用EasyMock.makeThreadSafe来使测试实例线程安全