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


704

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

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


2
我当时想就这个完全相同的问题发布一个问题。虽然Will在下面指出了许多优点,但我认为我们可以做得更好。我同意,没有一个单一的“方法”可以彻底解决这个问题。但是,“尽最大可能进行测试”将标准设置得很低。我将返回我的发现。
Zach Burlingame

在Java中:包java.util.concurrent包含一些错误的已知类,可能有助于编写确定性JUnit-Tests。看一看- CountDownLatch - 信号灯 - 交换
Synox

您能否提供到以前的单元测试相关问题的链接?
Andrew Grimm'5


7
我认为必须指出,这个问题已经有8年的历史了,与此同时,应用程序库也已经走了很长一段路。在“现代时代”(2016年)中,多线程开发主要出现在嵌入式系统中。但是,如果您使用的是台式机或电话应用程序,请先探索替代方法。像.NET这样的应用程序环境现在包括用于管理或大大简化90%的常见多线程方案的工具。(asnync / await,PLinq,IObservable,TPL ...)。多线程代码很难。如果您不重新发明轮子,则无需重新测试。
保罗·威廉姆斯

Answers:


245

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

处理复杂的多线程应用程序代码的最简单方法是:如果过于复杂而无法测试,则说明您做错了。如果您有一个实例,其中有多个线程在作用,并且您无法测试这些线程相互交叉的情况,则需要重新设计。它既简单又复杂。

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

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

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

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


1
如果您处理允许的语言/框架,那么代码分析将非常有用。EG:Findbugs将发现非常简单的静态变量共享并发问题。它找不到单例设计模式,它假设可以多次创建所有对象。对于像Spring这样的框架,此插件严重不足。
僵尸

3
实际上有一种解决方法:活动物体。drdobbs.com/parallel/prefer-using-active-objects-instead-of-n/…–
莳萝

6
虽然这是一个很好的建议,但我仍然问:“如何测试那些需要多个线程的最小区域?”
布莱恩·雷纳

5
“如果它太复杂以至于无法测试,则说明您做错了”-我们所有人都必须深入研究我们未编写的遗留代码。这种观察如何对任何人有确切的帮助?
罗娜

2
静态分析可能是一个好主意,但它不是测试。这篇文章确实没有回答关于如何测试的问题。
沃伦·露

96

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

kleolb02的答案很好。我会尝试更多细节。

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

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

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

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

除此之外(组件之间的测试交互)就是组件测试。同样在这种情况下,如果您坚持使用“谦虚对象”模式,则应该能够完全控制时序。


1
但是有时候,如果线程之间相互配合良好,那还应该进行一些测试,对吗?绝对可以,在阅读您的答案后,我将从异步部分中分离出核心逻辑。但是我仍然要通过一个异步的接口,对所有线程进行工作回调来测试逻辑。
CopperCash 2015年

那么多处理器系统呢?
Technophile

65

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

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

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

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

作为旁白:

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


51

近年来,当为多个项目编写线程处理代码时,我曾多次遇到此问题。我提供的答案较晚,因为在提供替代方案的同时,大多数其他答案实际上并未回答有关测试的问题。我的答案是针对多线程代码别无选择的情况。我会介绍代码设计的完整性,但同时也会讨论单元测试。

编写可测试的多线程代码

首先要做的是将生产线程处理代码与所有进行实际数据处理的代码分开。这样,可以将数据处理作为单线程代码进行测试,而多线程代码唯一要做的就是协调线程。

要记住的第二件事是,多线程代码中的错误是概率性的。最不经常出现的错误是会潜入生产中的错误,甚至在生产中也难以复制,因此会引起最大的问题。因此,对于多线程代码来说,快速编写代码然后对其进行调试直到可用的标准编码方法是个坏主意。这将导致在代码中修复了简单的错误,而危险的错误仍然存​​在。

相反,在编写多线程代码时,必须以一种避免编写错误的态度来编写代码。如果您正确删除了数据处理代码,则线程处理代码应该足够小-最好是几行,最坏的情况是几十行-您有机会在不编写错误的情况下编写代码,当然也不必编写许多错误,如果您了解线程技术,请花点时间并小心。

编写多线程代码的单元测试

一旦尽可能仔细地编写了多线程代码,仍然值得为该代码编写测试。测试的主要目的不是要测试高度时序相关的竞争条件错误-不可能重复测试此类竞争条件-而是测试您的防止此类错误的锁定策略是否允许多个线程按预期进行交互。

为了正确测试正确的锁定行为,测试必须启动多个线程。为了使测试可重复,我们希望线程之间的交互以可预测的顺序发生。我们不希望在测试中从外部同步线程,因为这将掩盖生产中线程未从外部同步的错误。这就将定时延迟用于线程同步,这是每当我不得不编写多线程代码测试时就成功使用的技术。

如果延迟太短,则测试会变得脆弱,因为微小的时序差异(例如,可能在其上运行测试的不同机器之间)可能会导致时序关闭且测试失败。我通常要做的是从导致测试失败的延迟开始,增加延迟以使测试在我的开发机器上可靠地通过,然后再将延迟加倍,从而使测试有很好的机会通过其他机器。这确实意味着测试将花费大量的时间,尽管以我的经验来看,仔细的测试设计可以将该时间限制为不超过12秒钟。由于您的应用程序中没有太多需要线程协调代码的地方,因此测试套件应该可以接受。

最后,跟踪测试所捕获的错误数量。如果您的测试具有80%的代码覆盖率,则可以预期它会捕获大约80%的错误。如果您的测试设计合理,但是没有发现错误,那么您很有可能没有其他仅会在生产环境中出现的错误。如果测试发现一两个错误,您可能仍然会很幸运。除此之外,您可能需要考虑仔细审查甚至完全重写线程处理代码,因为代码很可能仍包含隐藏的错误,这些错误很难在代码正式投入生产之前就发现,并且非常那很难解决。


3
测试只能显示错误的存在,而不能发现错误。最初的问题是关于2线程问题的,在这种情况下,可能会进行详尽的测试,但通常不是这样。对于最简单的场景以外的任何事情,您可能不得不硬着头皮并使用正式的方法-但不要跳过单元测试!首先,编写正确的多线程代码很困难,但同样困难的问题是,它需要面向未来进行验证,以防出现回归。
保罗·威廉姆斯

4
最少了解的方法之一的惊人总结。您的答案是对PPL通常忽略的真正隔离。
prash

1
即使您只进行了数百次相同长度的测试,一打秒也是相当长的时间……
Toby Speight

1
@TobySpeight与正常的单元测试相比,这些测试很长。我发现,如果适当地将线程代码设计为尽可能简单,那么六打测试就足够了,尽管-需要数百个多线程测试几乎可以肯定表明线程安排过于复杂。
沃伦·露

2
这是保持线程逻辑与功能尽可能独立的一个很好的论据(我知道,说起来容易做起来难)。而且,如果可能的话,将测试套件分为“每次更改”和“预先提交”集(这样,您的分钟到分钟的测试不会受到太大影响)。
Toby Speight,

22

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

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


20

周围有一些很好的工具。这是一些Java的摘要。

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

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

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

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

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


16

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

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

1
热情好客-正是我想要的!
Forge_7 2016年

14

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

引用:

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

...

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

...

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


13

我已经做了很多,是的。

一些技巧:

  • GroboUtils用于运行多个测试线程
  • alphaWorks竞赛可以检测类,以使插入在迭代之间有所不同
  • 创建一个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());
}

2
超时是个好主意,但如果测试超时,则该运行中任何以后的结果都是可疑的。超时测试可能仍在运行某些线程,这些线程可能使您陷入混乱。
唐·柯比

12

如前所述,测试MT代码的正确性是一个非常困难的问题。最后,归结为确保代码中没有不正确同步的数据竞争。这里的问题是,有线程执行(的交错),而且您没有太多的控制(请务必仔细阅读的无限多的可能性文章,虽然)。在简单的场景中,可能可能通过推理来证明正确性,但是通常不是这种情况。尤其是如果您要避免/最小化同步并且不希望使用最明显/最简单的同步选项。

我遵循的一种方法是编写高度并发的测试代码,以使可能发生的未被检测到的数据竞争成为可能。然后我运行了一段时间的测试:)我曾经偶然地在一个演讲中,有位计算机科学家在演示一种工具来进行此工作(根据规格随机地设计测试,然后疯狂地同时运行它们,检查定义的不变式)被打破)。

顺便说一句,我认为这里没有提到测试MT代码的这一方面:确定可以随机检查的代码不变式。不幸的是,找到那些不变式也是一个难题。同样,它们在执行期间可能不会一直保持有效,因此您必须在可以期望它们为真的地方找到/强制执行点。使代码执行达到这样的状态也是一个难题(它本身可能会引起并发问题。哎呀,这太难了!

一些有趣的链接阅读:


作者在测试中提到随机化。可能是QuickCheck,已移植到许多语言。你可以看对并发系统测试等谈话这里
马克斯

6

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

这个很难(硬。我采取了更简单的方法,并尝试使线程代码与实际测试无关。皮特(Pete)确实提到我的做法是错误的,但我要么已经正确分离了,要么我很幸运。


6
我阅读了到目前为止发表的两篇文章,但我发现它们并没有很大帮助。他只是谈论困难而没有给出太多具体建议。也许将来的文章会有所改善。
Don Kirkby

6

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

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


6

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

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

这种方法的最大局限性在于它只能让您测试怀疑会引起麻烦的方案。就像其他人所说的那样,您确实需要将多线程代码隔离为少量的简单类,以便有希望彻底测试它们。

一旦仔细测试了可能导致麻烦的场景,不科学的测试就会在类上同时抛出一堆并发的请求,这是查找意外麻烦的好方法。

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


5

我处理线程组件的单元测试的方式与处理任何单元测试的方式相同,即控制和隔离框架的反转。我在.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等中的内容使用相同的方法。


5

看看我的相关答案

为自定义屏障设计测试类

它偏向Java,但对选项有合理的总结。

总而言之(IMO),它不是使用某些可以确保正确性的精美框架,而是如何设计多线程代码。分散关注点(并发性和功能性)是提高信心的重要途径。由“测试引导”的“面向对象的增长型软件”比我能更好地解释一些选项。

可以选择静态分析和形式化方法(请参阅并发:状态模型和Java程序),但我发现它们在商业开发中用途有限。

不要忘记,很少保证任何负载/浸泡样式测试都能突出问题。

祝好运!


您还应该tempus-fugit在这里提及您的图书馆helps write and test concurrent code;)
Idolon 2013年

4

我最近才发现(对于Java)名为Threadsafe的工具。它是一个类似于findbugs的静态分析工具,但专门用于发现多线程问题。它不能替代测试,但是我可以推荐它作为编写可靠的多线程Java的一部分。

它甚至在类归类,使用并发类访问不安全的对象以及使用双重检查的锁定范例时发现丢失的volatile修饰符等方面都捕获了一些非常细微的潜在问题。

如果您编写多线程Java,请尝试一下。


3

以下文章提出了两种解决方案。包装信号量(CountDownLatch)并添加功能,例如外部化来自内部线程的数据。实现此目的的另一种方法是使用线程池(请参阅关注点)。

Sprinkler-高级同步对象


3
请在此说明方法,将来的外部链接可能会失效。
Uooo

2

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

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

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

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

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


2

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

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

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


1

假设在“多线程”代码下意味着

  • 有状态且易变
  • 由多个线程同时访问/修改

换句话说,我们正在谈论测试自定义有状态线程安全的类/方法/单元 -这在当今应该是非常罕见的野兽。

由于这种野兽很少见,因此我们首先要确保有所有有效的借口来编写它。

步骤1。考虑在相同的同步上下文中修改状态。

如今,编写可组合的并发和异步代码变得很容易,其中IO或其他慢速操作被卸载到后台,但是共享状态在一个同步上下文中被更新和查询。例如async / await任务和.NET中的Rx等-它们都可以通过设计进行测试,可以使用“真实”任务和调度程序来使测试具有确定性(但是这超出了问题的范围)。

听起来似乎很受限制,但是这种方法出奇地好。可以用这种样式编写整个应用程序,而无需使任何状态成为线程安全的(我愿意)。

步骤2.如果绝对不可能在单个同步上下文上操作共享状态。

确保轮子没有被重新发明/绝对没有标准的替代品可以适应这项工作。代码很可能具有很强的凝聚力,并且包含在一个单元中,例如,很有可能这是某些标准线程安全数据结构(如哈希映射或集合等)的特例。

注意:如果代码很大/跨多个类并且需要多线程状态操纵,那么很有可能设计不好,请重新考虑步骤1

步骤3.如果达到此步骤,那么我们需要测试自己的自定义有状态线程安全类/方法/单元

我会老实说:我从来不需要为此类代码编写适当的测试。在大多数情况下,我无法执行步骤1,有时可以执行步骤2。上一次不得不编写自定义线程安全代码的时间是很多年前,因此在我通过单元测试之前/可能不必编写它无论如何,以当前的知识。

如果我真的必须测试这样的代码(最后是实际答案),那么我将尝试以下几件事

  1. 非确定性压力测试。例如,同时运行100个线程,并检查最终结果是否一致。对于多用户场景的更高级别/集成测试,这更典型,但也可以在单元级别使用。

  2. 公开一些测试“钩子”,其中测试可以注入一些代码以帮助确定性方案,其中一个线程必须先执行另一个操作。尽管丑陋,但我想不出更好的办法。

  3. 延迟驱动测试,以使线程按特定顺序运行和执行操作。严格来说,此类测试也是不确定性的(系统冻结/ GC停止收集可能会扭曲原本精心策划的延迟),这也很丑陋,但可以避免钩子。


0

对于J2E代码,我使用SilkPerformer,LoadRunner和JMeter进行线程的并发测试。他们都做同样的事情。基本上,它们为您提供了一个相对简单的界面,用于管理其代理服务器的版本,这是必需的,以便分析TCP / IP数据流并模拟同时向您的应用程序服务器发出请求的多个用户。代理服务器可以在处理请求之后,通过显示发送到服务器的整个页面和URL以及服务器的响应,使您能够执行诸如分析请求的操作。

您可以在不安全的http模式下找到一些错误,在这里至少可以分析正在发送的表单数据,并系统地更改每个用户的表单数据。但是真正的测试是当您在https(安全套接字层)中运行时。然后,您还必须应对系统地更改会话和cookie数据的问题,这可能会有些麻烦。

在测试并发性时,我发现的最好的错误是,当我发现开发人员在登录时依赖Java垃圾收集来关闭登录时建立的与LDAP服务器的连接请求时,这导致用户暴露在外。尝试分析服务器瘫痪时发生的情况时,每隔几秒钟便几乎无法完成一项事务,从而导致其他用户的会话和令人困惑的结果。

最后,您或某人可能必须屈服并分析类似我刚才提到的错误的代码。在我们展开上述问题时,跨部门的公开讨论(如发生的讨论)最为有用。但是这些工具是测试多线程代码的最佳解决方案。JMeter是开源的。SilkPerformer和LoadRunner是专有的。如果您真的想知道您的应用程序是否是线程安全的,那么大个子就是这样做的。我已经为大型公司专业地做到了这一点,所以我没有猜测。我是根据个人经验讲的。

请注意:了解这些工具确实需要一些时间。除非您已经对多线程编程有所了解,否则不必简单地安装软件并启动GUI。我已尝试确定需要理解的3个关键领域(表单,会话和Cookie数据),希望至少从理解这些主题开始将有助于您专注于快速结果,而不必通读整个文档。


0

并发是内存模型,硬件,缓存和我们的代码之间的复杂相互作用。在Java的情况下,至少这些测试主要由jcstress进行了部分解决。众所周知,该库的创建者是许多JVM,GC和Java并发功能的作者。

但是即使这个库也需要Java内存模型规范的丰富知识,以便我们确切地知道我们正在测试什么。但是我认为这项工作的重点是微基准测试。没有巨大的业务应用程序。


0

有一篇关于该主题的文章,示例代码中使用Rust作为语言:

https://medium.com/@polyglot_factotum/rust-concurrency-five-easy-pieces-871f1c62906a

总之,诀窍是编写并发逻辑,以便使用通道和condvars之类的工具对涉及多个执行线程的不确定性具有鲁棒性。

然后,如果这是构造“组件”的方式,那么测试它们的最简单方法是使用通道向它们发送消息,然后阻止其他通道断言该组件发送了某些预期的消息。

链接到的文章使用单元测试完全编写。


-1

如果您正在测试简单的新Thread(runnable).run() 可以模拟Thread来依次运行该runnable

例如,如果测试对象的代码调用了这样的新线程

Class TestedClass {
    public void doAsychOp() {
       new Thread(new myRunnable()).start();
    }
}

然后模拟新线程并依次运行runnable参数可以帮助

@Mock
private Thread threadMock;

@Test
public void myTest() throws Exception {
    PowerMockito.mockStatic(Thread.class);
    //when new thread is created execute runnable immediately 
    PowerMockito.whenNew(Thread.class).withAnyArguments().then(new Answer<Thread>() {
        @Override
        public Thread answer(InvocationOnMock invocation) throws Throwable {
            // immediately run the runnable
            Runnable runnable = invocation.getArgumentAt(0, Runnable.class);
            if(runnable != null) {
                runnable.run();
            }
            return threadMock;//return a mock so Thread.start() will do nothing         
        }
    }); 
    TestedClass testcls = new TestedClass()
    testcls.doAsychOp(); //will invoke myRunnable.run in current thread
    //.... check expected 
}

-3

(如果可能)不要使用线程,而应使用actor /活动对象。易于测试。


2
@OMTheEternity也许是它的最佳答案,但imo。
Dill 2015年

-5

您可以使用EasyMock.makeThreadSafe使测试实例成为线程安全的


这根本不是测试多线程代码的可能方法。问题不在于测试代码运行多线程,而是测试通常运行多线程的代码。而且您无法同步所有内容,因为那样您实际上就不再测试数据竞争。
bennidi 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.