我应该如何对线程代码进行单元测试?

到目前为止,我彷佛避免了测试多线程代码的噩梦,由于它彷佛太多了。 我想问一下人们如何去测试依赖于线程的代码才能成功执行,或者人们如何去测试那些仅在两个线程以给定方式交互时才会出现的问题? java

对于当今的程序员来讲,这彷佛是一个很是关键的问题,将咱们的知识集中在这一恕我直言中将颇有用。 git


#1楼

我曾经承担过测试线程代码的不幸任务,它们绝对是我编写过的最难的测试。 程序员

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

我不肯定这是不是最好的方法,可是它对我来讲是可行的。 编程


#2楼

确实很强悍! 在个人(C ++)单元测试中,按照使用的并发模式将其分为几类: api

  1. 对在单个线程中运行且不了解线程的类进行单元测试-轻松进行常规测试。 安全

  2. 暴露公开的公共API的Monitor对象 (在调用者的控制线程中执行同步方法的对象)的单元测试-实例化使用该API的多个模拟线程。 构造适用于被动对象内部条件的方案。 包括一个运行时间更长的测试,该测试基本上能够长时间消除来自多个线程的麻烦。 我知道这是不科学的,但确实创建了信心。 多线程

  3. Active对象 (封装了本身的一个或多个控制线程的对象)的单元测试-与上面的#2类似,具体取决于类设计。 公共API可能处于阻塞状态或非阻塞状态,调用者可能会获取期货,数据可能会到达队列或须要出队。 这里有不少组合。 白盒子走了。 仍然须要多个模拟线程来调用被测对象。 并发

做为旁白: 框架

在我进行的内部开发人员培训中,我讲授并发支柱和这两种模式做为思考和分解并发问题的主要框架。 显然还有更高级的概念,可是我发现这套基础知识有助于使工程师远离困境。 如上所述,它还会致使代码更可单元测试。


#3楼

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

处理复杂的多线程应用程序代码的最简单方法是:若是过于复杂而没法测试,则说明您作错了。 若是您有一个实例,该实例具备做用于其上的多个线程,而且您没法测试这些线程彼此间步调一致的状况,则须要从新设计。 它既简单又复杂。

为多线程编程的方法有不少,能够避免线程同时在实例中运行。 最简单的是使全部对象不可变。 固然,一般是不可能的。 所以,您必须肯定设计中线程与同一实例交互的那些位置,并减小这些位置的数量。 经过这样作,您隔离了实际发生多线程的几个类,从而下降了测试系统的整体复杂性。

可是您必须意识到,即便这样作,您仍然没法测试两个线程相互踩踏的全部状况。 为此,您必须在同一测试中同时运行两个线程,而后精确控制它们在任何给定时刻执行的行。 您能作的最好的就是模拟这种状况。 但这可能须要您专门为测试编写代码,而这仅是迈向真正解决方案的一半。

测试代码是否存在线程问题的最佳方法多是对代码进行静态分析。 若是您的线程代码没有遵循一组有限的线程安全模式,那么您可能会遇到问题。 我相信VS中的代码分析确实包含一些线程知识,但可能很少。

看起来,就目前而言(可能会好时机),测试多线程应用程序的最佳方法是尽量下降线程代码的复杂性。 尽可能减小线程交互的区域,尽量地进行测试,并使用代码分析来识别危险区域。


#4楼

我已经作了不少,是的。

一些技巧:

  • 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)} 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());
}

#5楼

测试(一般)测试线程代码(一般是很是复杂的系统)的另外一种方法是经过Fuzz Testing 。 它不是很好,而且没法找到全部内容,可是它可能颇有用且操做简单。

引用:

模糊测试或模糊测试是一种软件测试技术,可为程序的输入提供随机数据(“模糊”)。 若是程序失败(例如,因为崩溃或内置代码断言失败),则能够指出缺陷。 模糊测试的最大优势是测试设计很是简单,而且没有对系统行为的先入之见。

...

模糊测试一般用于采用黑匣子测试的大型软件开发项目中。 这些项目一般有预算来开发测试工具,而模糊测试是提供高性价比的技术之一。

...

可是,模糊测试不能替代详尽的测试或形式化方法:它只能提供系统行为的随机样本,而且在许多状况下,经过模糊测试可能仅代表某软件能够处理异常而不会崩溃,而不是行为正确。 所以,模糊测试只能被视为发现错误的工具,而不能保证质量。

相关文章
相关标签/搜索