multithreading - 如何单元测试线程代码?

  显示原文与译文双语对照的内容

Hot-on-the-heels上一个单元测试相关问题,这里是另一个 toughie:

至此,我已经避免了测试multi-threaded代码的噩梦,因为它看起来太像雷区了。 我想问人们如何测试依赖线程的代码,或者是人们如何测试那些只有在两个线程以给定方式交互时才会出现的问题?

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

时间:

听着,没有简单的方法来做这个。 我正在处理一个固有多线程的项目。 事件来自操作系统,我必须同时处理它们。

处理复杂的multithhreaded应用程序代码的最简单方法是: 如果它太复杂,无法测试,那么你就做错了。 如果有一个单独的实例有多个线程作用,并且你不能测试这些线程之间的相互作用,那么你的设计需要重做。 它既简单又复杂。

多线程编程的方法有很多,它避免了线程同时通过实例运行。 最简单的方法是使所有对象不可变。 当然,这通常不可能。 所以你必须在你的设计中确定那些线程interract具有相同实例的地方,并减少这些地方的数量。 通过这样做,你可以隔离几个实际发生多线程的类,从而降低测试你的系统的总体复杂性。

但是你必须意识到即使这样做,你仍然不能测试每一个线程在彼此之间的相互作用。 为此,你必须在同一个测试中同时运行两个线程,然后精确控制它们在任何给定时刻执行的行。 你可以做的就是模拟这种情况。 但是这可能需要你专门为测试而编写代码,而这充其量是迈向真正解决方案的一半。

测试线程问题的最好方法可能是通过静态分析代码。 如果线程代码没有遵循有限的线程安全模式集,那么你可能会遇到问题。 我相信 vs 中的代码分析的确包含了线程的一些知识,但可能并没有多少。

听着,随着目前( 可能会站在一个好的时间来)的发展,测试多线程应用的最佳方法是尽可能减少线程代码的复杂性。 最小化线程交互的区域,尽可能最好地测试,并使用代码分析来识别危险区域。

这个问题已经过了一段时间,但仍然没有应答。。

kleolb02的答案是一个不错的答案。 我会尝试更多细节。

有一种方法,我为 C# 代码实践。 对于单元测试,你应该能够对进行程序测试,这是多线程代码中最大的挑战。 所以我的回答是将异步代码强制到一个测试工具中,它可以同步地运行 。

这是来自 meszardos" xUnit测试模式"的一个想法,叫做"humble对象"( ) 。 695 ): 你必须分离核心逻辑代码,以及任何气味,比如异步代码。 这将导致一个核心逻辑的类,它可以同步地运行 。

这将使你能够在一个同步方法中测试核心逻辑代码。 你可以对核心逻辑的调用时间进行绝对控制,从而使可以再现测试。 这就是分离核心逻辑和异步逻辑的好处。

这个核心逻辑需要被缠绕在另一个类,它负责接收异步调用核心逻辑,代表这些调用的核心逻辑。 生产代码只能通过该类访问核心逻辑。 因为这个类应该只委托调用,所以它是一个没有太多逻辑的"哑"类。 这样,你就可以在最少的时间保持这个异步工作类的单元测试。

上( 测试类之间的交互) 是组件测试。 在这种情况下,如果你坚持"humble对象"模式,你应该能够对时间进行绝对控制。

真的很困难在我的( C++ ) 单元测试中,我沿着使用的并发模式的几行分成几个类别: !

  1. 在单个线程中运行的类测试,而不是线程感知--容易,测试如常。

  2. 监视器对象的单元测试对象 ( 在调用者中执行同步方法的'线程控制一个同步的公共 API --实例化多个模拟线程的模拟线程。 构造运行被动对象内部条件的场景。 包括一个长时间运行的测试,它基本上在很长一段时间内从多线程中打败了它。 这是不科学的,但它确实建立了信任。

  3. 单元测试活动对象 ( 那些封装自己的线程或者控制线程的) --类似于前面 #2 变化取决于类的设计。 公共API可能阻塞或者 non-blocking,调用者可能获取期货,数据可能到达队列或者需要 dequeued 。 这里有很多组合,白色的盒子。 仍然需要多个模拟线程调用正在测试的对象。

作为旁白:

在内部开发人员培训我,我教的支柱并发和这两个模式作为考虑的主要框架和分解的并发问题。 显然有更高级的概念,但我发现这一套基本的概念有助于工程师摆脱 soup 。 它还导致了更多的单元测试,如上所述。

我在测试multi-线程代码时也遇到严重问题。 然后我在"xunit测试模式"中找到了一个非常酷的解决方案,Gerard Meszaros 。 他描述的模式叫做的对象

基本上它描述了如何将逻辑提取到一个独立的easy-to-test组件中,该组件与它的环境分离。 在你测试这个逻辑之后,你可以测试复杂的行为( multi-线程,异步执行,等等 - )

对于. NET,你可以查看微软项目国际象棋编辑器的研究

这里有一些工具很不错。 下面是一些Java的总结。

一些好的静态分析工具包括 FindBugs ( 给出一些有用的提示),JLint, java探路者 ( JPF & JPF2 ), 和 Bogor

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

来自IBM研究的竞赛是有趣的。 它通过插入各种线程修改行为( 例如。) 来检测代码。 睡眠 & yield ) 尝试随机发现 Bug 。

SPIN 是一个非常酷的工具来建模你的Java ( 等等) 组件,但是你需要有一些有用的框架。 它很难使用,但如果你知道如何使用它,它非常强大。 很多工具在引擎盖下面使用旋转。

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

我做了很多这样的事情,而且是真的。

一些提示:

  • 用于运行多个测试线程的GroboUtils
  • alphaWorks竞赛到仪器类,使交错在不同迭代之间变化
  • 创建一个 throwable 字段并在 tearDown ( 参见清单 1 ) 中检查它。 如果在另一个线程中捕获了一个错误的异常,只需将它分配给 throwable 。
  • 我在清单 2中创建了utils类,并发现它非常宝贵,特别是waitForVerify和 waitForCondition,这将极大地提高测试的性能。
  • 在测试中充分利用 AtomicBoolean 。 它是线程安全的,你通常需要一个最终的引用类型来存储回调类及其内容的值。 参见清单 3中的示例。
  • 一定要给你的测试一个超时的( e.g,@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 <code>null</code>
 * 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. <code>null</code> 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 <code>true</code> 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());
}

刚刚在上发布了这个有用的文章 multi-threaded Java应用程序的单元测试。

来自本文的库的JUnit 4版本在这里: MultiThreadedTC-JUnit4

Goodliffe单元测试线程的代码。

这很困难。我使用更简单的方法,尝试保持线程代码从实际测试中抽象出来。 皮特提到我这样做是错的,但我要么是分离了要么我只是幸运。

我处理线程组件的单元测试与处理任何 单元测试的方式相同,这就是控制和隔离框架的反转。 我在. 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注入到我的组件中,并使用我的隔离框架来使线程在测试过程中表现得像预期的那样。

这对我来说很好,我对线程池,系统,睡眠 等等 等使用同样的方法。

...