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

时间:2020-03-05 18:39:51  来源:igfitidea点击:

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

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

解决方案

回答

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

在编写测试时,我结合使用了委托和事件。从根本上讲,这就是将" PropertyNotifyChanged"事件与" WaitCallback"或者某种" ConditionalWaiter"一起使用。

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

回答

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

  • 对在单个线程中运行并且不了解线程的类进行单元测试-轻松进行常规测试。
  • 暴露公开的公共API的Monitor对象(在调用者的控制线程中执行同步方法的对象)的单元测试-实例化使用该API的多个模拟线程。构造适用于被动对象内部条件的方案。包括一个运行时间更长的测试,该测试基本上可以长时间消除来自多个线程的麻烦。我知道这是不科学的,但确实可以建立信心。
  • Active对象(封装了自己的一个或者多个控制线程的对象)的单元测试-与上面的#2相似,具体取决于类设计。公共API可能处于阻塞状态或者非阻塞状态,调用者可能会获取期货,数据可能会到达队列或者需要出队。这里有很多组合。白盒子走了。仍然需要多个模拟线程来对被测对象进行调用。

作为旁白:

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

回答

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

处理复杂的,被多方面理解的应用程序代码的最简单方法是:如果过于复杂而无法测试,则说明我们做错了。如果我们有一个实例,该实例具有作用于其上的多个线程,并且我们无法测试这些线程彼此步履蹒跚的情况,则需要重做设计。它既简单又复杂。

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

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

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

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

回答

皮特·古德利夫(Pete Goodliffe)对线程代码的单元测试进行了一系列测试。

这个很难(硬。我采取了更简单的方法,并尝试将线程代码从实际测试中抽象出来。皮特(Pete)确实提到我的做法是错误的,但我或者已经正确分离,或者我很幸运。

回答

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

基本上,它描述了如何将逻辑提取到与环境分离的独立,易于测试的组件中。在测试了该逻辑之后,我们可以测试复杂的行为(多线程,异步执行等)。

回答

我喜欢编写两个或者多个测试方法以在并行线程上执行,并且每个方法都对被测对象进行调用。我一直在使用Sleep()调用来协调来自不同线程的调用顺序,但这并不是很可靠。这也要慢很多,因为我们必须睡足够长的时间才能正常进行计时。

我从编写FindBugs的同一个小组中找到了多线程TC Java库。它使我们无需使用Sleep()即可指定事件的顺序,并且它是可靠的。我还没有尝试过。

这种方法的最大局限性在于它只能让我们测试怀疑会引起麻烦的方案。正如其他人所说,我们确实需要将多线程代码隔离为少数几个简单的类,以期有希望对其进行全面测试。

一旦仔细测试了预期会造成麻烦的场景,那么不科学的测试就会在类上同时抛出一堆同时的请求,这是查找意外麻烦的好方法。

更新:我已经使用了多线程TC Java库,并且运行良好。我还将其某些功能移植到了我称为TickingTest的.NET版本中。

回答

对于Java,请查阅JCIP的第12章。有一些具体的例子,编写确定性的多线程单元测试,至少可以测试并发代码的正确性和不变性。

用单元测试"证明"线程安全性要好得多。我相信,通过在各种平台/配置上进行自动集成测试可以更好地解决此问题。

回答

我已经做了很多,是的。

一些技巧:

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

回答

通常,测试(测试)线程代码和非常复杂的系统的另一种方法是通过Fuzz测试。
它不是很好,并且无法找到所有内容,但是它可能很有用且操作简单。

引用:

Fuzz testing or fuzzing is a software testing technique that provides random data("fuzz") to the inputs of a program. If the program fails (for example, by crashing, or by failing built-in code assertions), the defects can be noted. The great advantage of fuzz testing is that the test design is extremely simple, and free of preconceptions about system behavior. 
  
  ...
  
  Fuzz testing is often used in large software development projects that employ black box testing. These projects usually have a budget to develop test tools, and fuzz testing is one of the techniques which offers a high benefit to cost ratio. 
  
  ...
  
  However, fuzz testing is not a substitute for exhaustive testing or formal methods: it can only provide a random sample of the system's behavior, and in many cases passing a fuzz test may only demonstrate that a piece of software handles exceptions without crashing, rather than behaving correctly. Thus, fuzz testing can only be regarded as a bug-finding tool rather than an assurance of quality.

回答

提这个问题已经有一段时间了,但仍然没有答案...

kleolb02的答案很好。我将尝试详细介绍。

我为Ccode练习了一种方法。对于单元测试,我们应该能够编写可重现的测试,这是多线程代码中的最大挑战。因此,我的答案旨在将异步代码强制放入可同步工作的测试工具中。

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

这使我们能够以同步方式测试核心逻辑代码。我们可以完全控制在核心逻辑上执行呼叫的时间,因此可以进行可重复的测试。这是将核心逻辑和异步逻辑分离的好处。

该核心逻辑需要由另一个类包装,该类负责异步接收对核心逻辑的调用,并将这些调用委托给核心逻辑。生产代码将仅通过该类访问核心逻辑。因为此类仅应委托调用,所以它是一个非常"愚蠢"的类,没有太多逻辑。因此,我们可以使此异步工作类的单元测试最少。

超出此范围(类之间的测试交互)的任何内容都是组件测试。同样在这种情况下,如果我们坚持使用"谦虚对象"模式,则应该能够完全控制时序。

回答

我上周的大部分时间都在大学图书馆里学习并发代码的调试。中心问题是并发代码是不确定的。通常,学术调试属于以下三个阵营之一:

  • 事件跟踪/重播。这需要一个事件监视器,然后检查已发送的事件。在UT框架中,这将涉及作为测试的一部分手动发送事件,然后进行事后审查。
  • 可编写脚本。在这里,我们可以通过一组触发器与正在运行的代码进行交互。 "在x> foo上,baz()"。可以将其解释为UT框架,在该框架中,我们将具有在特定条件下触发给定测试的运行时系统。
  • 交互的。这显然在自动测试情况下不起作用。 ;)

现在,正如上面的评论者所注意到的,我们可以将并发系统设计为更具确定性的状态。但是,如果操作不正确,我们将再次回到设计顺序系统。

我的建议是专注于就什么线程化和什么不线程化制定非常严格的设计协议。如果我们对界面进行约束,以使元素之间的相关性降到最低,那么它会容易得多。

祝我们好运,并继续努力解决问题。

回答

我处理线程组件的单元测试的方式与处理任何单元测试的方式相同,即控制和隔离框架的反转。我在.Net领域中开发,开箱即用的线程(除其他外)很难(要说几乎不可能)完全隔离。

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

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的摘要。

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

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

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

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

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

回答

等待性还可以编写确定性的单元测试。它使我们可以等待,直到系统中某处的某些状态被更新为止。例如:

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