到目前为止,我已经避免了测试多线程代码的噩梦,因为它看起来像是一个雷区太多了.我想问一下人们如何测试依赖线程成功执行的代码,或者人们如何测试那些只在两个线程以给定方式交互时出现的问题?
对于今天的程序员来说,这似乎是一个非常关键的问题,将我们的知识集中在这个imho上是有用的.
看,没有简单的方法可以做到这一点.我正在开发一个本质上是多线程的项目.事件来自操作系统,我必须同时处理它们.
处理复杂的多线程应用程序代码的最简单方法是:如果它太复杂而无法测试,那么你做错了.如果您有一个具有多个线程的单个实例,并且您无法测试这些线程相互重叠的情况,那么您的设计需要重做.它既简单又复杂.
有许多方法可以为多线程编程,避免线程同时运行在实例中.最简单的方法是使所有对象都不可变.当然,这通常是不可能的.因此,您必须在设计中识别线程与同一实例相互作用并减少这些位置数量的位置.通过这样做,您可以隔离实际发生多线程的几个类,从而降低测试系统的整体复杂性.
但是你必须意识到,即使通过这样做,你仍然无法测试两个线程相互衔接的每种情况.要做到这一点,你必须在同一个测试中同时运行两个线程,然后在任何给定时刻准确控制它们正在执行的行.你能做的最好的就是模拟这种情况.但这可能需要您专门为测试编写代码,而这最多只是迈向真正解决方案的一半.
测试线程问题代码的最佳方法可能是通过代码的静态分析.如果您的线程代码不遵循一组有限的线程安全模式,那么您可能会遇到问题.我相信VS中的Code Analysis确实包含一些线程知识,但可能并不多.
看,就目前的状况而言(并且可能会有好的时机),测试多线程应用程序的最佳方法是尽可能降低线程代码的复杂性.最小化线程交互的区域,尽可能测试,并使用代码分析来识别危险区域.
发布这个问题已经有一段时间了,但仍然没有回答......
kleolb02的答案很好.我会尝试详细介绍.
有一种方法,我为C#代码练习.对于单元测试,您应该能够编写可重现的测试,这是多线程代码中的最大挑战.所以我的答案旨在将异步代码强制转换为同步工作的测试工具.
这是Gerard Meszardos的书" xUnit Test Patterns "中的一个想法,被称为"Humble Object"(第695页):你必须将核心逻辑代码和任何闻起来像异步代码的东西分开.这将导致核心逻辑的类,它同步工作.
这使您能够以同步方式测试核心逻辑代码.您可以完全控制对核心逻辑执行的调用的时间,从而可以进行可重复的测试.这是分离核心逻辑和异步逻辑的好处.
这个核心逻辑需要由另一个类包围,该类负责异步接收对核心逻辑的调用,并将这些调用委托给核心逻辑.生产代码只能通过该类访问核心逻辑.因为这个类只应该委托调用,所以它是一个非常"愚蠢"的类,没有太多的逻辑.因此,您可以至少对这个非同步工作级别进行单元测试.
上面的任何内容(测试类之间的交互)都是组件测试.同样在这种情况下,如果您坚持使用"Humble Object"模式,您应该能够对时间进行绝对控制.
确实很难!在我的(C++)单元测试中,我将其分解为使用的并发模式的几个类别:
对在单个线程中运行且不是线程感知的类的单元测试 - 容易,像往常一样进行测试.
Monitor对象(在调用者的控制线程中执行同步方法的对象)的单元测试,它们公开同步的公共API - 实例化多个运行API的模拟线程.构建行使被动对象内部条件的场景.包括一个长时间运行的测试,它基本上可以在很长一段时间内从多个线程中击败它.我知道这是不科学的,但它确实建立了信心.
Active对象的单元测试(封装自己的线程或控制线程的对象) - 类似于上面的#2,具有取决于类设计的变化.公共API可能是阻塞的或非阻塞的,呼叫者可能获得期货,数据可能到达队列或需要出列.这里有许多可能的组合; 白盒子走了.仍然需要多个模拟线程来调用被测对象.
作为旁白:
在我所做的内部开发人员培训中,我教授并发支柱和这两种模式作为思考和分解并发问题的主要框架.显然有更先进的概念,但我发现这套基础知识有助于让工程师摆脱困境.如上所述,它还导致代码更可单元测试.
近几年来,我为几个项目编写线程处理代码时,我多次遇到过这个问题.我提供了一个迟到的答案,因为大多数其他答案在提供替代方案时,实际上并没有回答有关测试的问题.我的答案是针对除了多线程代码之外没有其他选择的情况; 我确实涵盖了代码设计问题的完整性,但也讨论了单元测试.
编写可测试的多线程代码
首先要做的是将生产线程处理代码与执行实际数据处理的所有代码分开.这样,数据处理可以作为单线程代码进行测试,多线程代码唯一能做的就是协调线程.
要记住的第二件事是多线程代码中的错误是概率性的; 最不经常表现出来的错误是潜入生产的错误,即使在生产中也难以复制,从而导致最大的问题.出于这个原因,快速编写代码然后调试它直到它工作的标准编码方法对于多线程代码来说是一个坏主意; 这将导致代码中的容易错误被修复,危险的错误仍然存在.
相反,在编写多线程代码时,您必须以一种态度编写代码,以避免首先编写错误.如果你已经正确删除了数据处理代码,那么线程处理代码应该足够小 - 最好是几行,最差几十行 - 你有机会编写它而不会编写bug,当然也没有写出很多bug ,如果你理解穿线,花点时间,并且小心.
编写单元测试多线程代码
一旦尽可能仔细地编写多线程代码,仍然值得为该代码编写测试.测试的主要目的不是测试高度时序相关的竞争条件错误 - 不可能重复测试这种竞争条件 - 而是测试你的防止这种错误的锁定策略允许多个线程按预期进行交互.
要正确测试正确的锁定行为,测试必须启动多个线程.为了使测试可重复,我们希望线程之间的交互以可预测的顺序发生.我们不希望外部同步测试中的线程,因为这将掩盖在生产中可能发生的线程未在外部同步的错误.这使得线程同步使用了时序延迟,这是我在必须编写多线程代码测试时成功使用的技术.
如果延迟太短,则测试变得脆弱,因为较小的时序差异 - 例如在可能运行测试的不同机器之间 - 可能导致时序关闭而测试失败.我通常做的是从导致测试失败的延迟开始,增加延迟以便测试在我的开发机器上可靠地通过,然后将延迟加倍,以便测试很有可能传递到其他机器上.这确实意味着测试需要花费大量的时间,但根据我的经验,仔细的测试设计可以将时间限制在不超过十几秒.由于您的应用程序中不应该有很多需要线程协调代码的地方,因此您的测试套件应该可以接受.
最后,跟踪测试中捕获的错误数量.如果您的测试具有80%的代码覆盖率,则可能会捕获大约80%的错误.如果您的测试设计合理但没有发现错误,那么您有可能没有其他只会出现在生产中的错误.如果测试捕获了一个或两个错误,您可能仍然很幸运.除此之外,您可能需要仔细检查甚至完全重写您的线程处理代码,因为代码仍然可能包含在代码生产之前很难找到的隐藏错误,并且非常那么很难解决.
我在测试多线程代码时也遇到了严重的问题.然后我在Gerard Meszaros的"xUnit Test Patterns"中找到了一个非常酷的解决方案.他描述的模式称为Humble对象.
基本上它描述了如何将逻辑提取到一个与环境分离的独立,易于测试的组件中.在测试了这个逻辑之后,您可以测试复杂的行为(多线程,异步执行等......)
有一些工具非常好.以下是一些Java的摘要.
一些好的静态分析工具包括FindBugs(提供一些有用的提示),JLint,Java Pathfinder(JPF和JPF2)和Bogor.
MultithreadedTC是一个非常好的动态分析工具(集成到JUnit中),您必须在其中设置自己的测试用例.
IBM Research的ConTest非常有趣.它通过插入各种线程修改行为(例如睡眠和产量)来检测代码,以尝试随机发现错误.
SPIN是一个非常酷的工具,用于建模Java(和其他)组件,但您需要有一些有用的框架.它很难按原样使用,但如果你知道如何使用它会非常强大.相当多的工具在引擎盖下使用SPIN.
多线程TC可能是最主流的,但上面列出的一些静态分析工具绝对值得关注.
Awaitility也可以帮助您编写确定性单元测试.它允许您等到系统中的某个状态更新.例如:
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
(有点)测试线程代码的另一种方法,以及一般非常复杂的系统是通过Fuzz Testing.它不是很好,它不会找到所有东西,但它可能很有用而且很容易做到.
引用:
模糊测试或模糊测试是一种软件测试技术,可为程序的输入提供随机数据("模糊").如果程序失败(例如,通过崩溃或内置代码断言失败),则可以注意到缺陷.模糊测试的巨大优势在于测试设计非常简单,并且没有对系统行为的偏见.
...
模糊测试通常用于采用黑盒测试的大型软件开发项目中.这些项目通常有预算来开发测试工具,而模糊测试是提供高成本效益的技术之一.
...
然而,模糊测试不能代替详尽的测试或正式方法:它只能提供系统行为的随机样本,并且在许多情况下通过模糊测试可能只能证明一个软件处理异常而不会崩溃,而不是行为正确.因此,模糊测试只能被视为错误查找工具,而不是质量保证.
我做了很多这个,是的很糟糕.
一些技巧:
GroboUtils用于运行多个测试线程
alphaWorks ConTest用于检测类,以使迭代之间的交错变化
创建一个throwable
字段并将其签入tearDown
(参见清单1).如果你在另一个线程中捕获到一个错误的异常,只需将它分配给throwable.
我在清单2中创建了utils类,并发现它非常有价值,特别是waitForVerify和waitForCondition,这将大大提高测试的性能.
AtomicBoolean
在你的测试中充分利用.它是线程安全的,你经常需要一个最终的引用类型来存储来自回调类等的值.请参阅清单3中的示例.
确保始终为测试提供超时(例如@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)} withnull
* 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; } /** Returnstrue
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()); }
正如已经说明的那样,测试MT代码的正确性是一个非常难的问题.最后,它归结为确保代码中没有错误同步的数据争用.这里的问题是,有线程执行(的交错),而且您没有太多的控制(请务必仔细阅读的无限多的可能性此文章,虽然).在简单的情况下,可能通过推理实际证明正确性,但通常情况并非如此.特别是如果你想避免/最小化同步而不是最明显/最简单的同步选项.
我遵循的方法是编写高度并发的测试代码,以便可能发生可能未检测到的数据争用.然后我运行那些测试一段时间:)我曾经偶然发现一个谈话,其中一些计算机科学家展示了一种工具,这种方式(从规范中随机设计测试,然后同时运行它们,同时检查定义的不变量)被打破).
顺便说一句,我认为这里没有提到测试MT代码的这方面:识别可以随机检查的代码的不变量.不幸的是,找到那些不变量也是一个很难的问题.此外,他们可能无法在执行期间保持所有时间,因此您必须找到/强制执行可以预期它们为真的执行点.将代码执行带到这样的状态也是一个难题(并且本身可能会引发并发问题.哇,这太难了!
一些有趣的链接:
确定性交织:允许强制某些线程交错然后检查不变量的框架
jMock Blitzer:压力测试同步
assertConcurrent:JUnit版本的压力测试同步
测试并发代码:对暴力(压力测试)或确定性(针对不变量)的两种主要方法的简要概述
Pete Goodliffe有一系列关于线程代码的单元测试.
这个很难(硬.我采取了更简单的方法,并尝试保持从实际测试中抽象出来的线程代码.皮特确实提到我这样做的方式是错的,但我要么正确分离,要么就是幸运.
对于Java,请查看JCIP的第12章.编写确定性,多线程单元测试以至少测试并发代码的正确性和不变量有一些具体的例子.
"通过单元测试证明"螺纹安全性更加严格.我的信念是,通过各种平台/配置的自动化集成测试可以更好地满足这一需求.
我喜欢编写两个或多个测试方法来在并行线程上执行,并且每个测试方法都会调用被测对象.我一直在使用Sleep()调用来协调来自不同线程的调用顺序,但这并不可靠.它也慢得多,因为你必须睡得足够长,以至于时间通常有效.
我在编写FindBugs的同一组中找到了多线程TC Java库.它允许您在不使用Sleep()的情况下指定事件的顺序,并且它是可靠的.我还没有尝试过.
这种方法的最大限制是它只允许您测试您怀疑会导致麻烦的场景.正如其他人所说的那样,你真的需要将多线程代码隔离成少量简单的类,以便彻底测试它们.
一旦你仔细测试了你希望引起麻烦的场景,一个不科学的测试会在课堂上抛出一堆同时发出的请求,这是寻找意外麻烦的好方法.
更新:我使用多线程TC Java库玩了一下,效果很好.我还将其部分功能移植到我称之为TickingTest的.NET版本中.
我处理线程组件的单元测试与处理任何单元测试的方式相同,即控制和隔离框架的反转.我在.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等等.
看看我的相关答案
为自定义屏障设计测试类
它偏向于Java,但对选项有一个合理的总结.
总而言之虽然(IMO)它不是使用一些确保正确性的花哨框架,而是如何设计多线程代码.分散关注点(并发性和功能性)对提高信心起着重要作用.以测试为导向的不断增长的面向对象软件解释了一些比我更好的选择.
静态分析和形式化方法(参见,并发:状态模型和Java程序)是一种选择,但我发现它们在商业开发中的用途有限.
不要忘记任何加载/浸泡样式测试很少能保证突出问题.
祝好运!