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

大憨熊 提交于 2019-12-25 18:50:03

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>>

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

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


#1楼

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

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

我不确定这是否是最好的方法,但是它对我来说是可行的。


#2楼

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

  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 。 它不是很好,并且无法找到所有内容,但是它可能很有用且操作简单。

引用:

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

...

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

...

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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!