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

How should I unit test threaded code?

到目前为止,我已经避免了测试多线程代码的噩梦,因为它似乎太像雷区了。我想问人们是如何测试依赖线程来成功执行的代码的,还是人们如何测试那些只在两个线程以给定方式交互时才会出现的问题?

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


听着,要做到这一点不容易。我正在开发一个固有的多线程项目。事件来自操作系统,我必须同时处理它们。

处理复杂的多线程应用程序代码的最简单方法是:如果它太复杂而无法测试,那么您就错了。如果有一个实例有多个线程在其上运行,并且无法测试这些线程相互跨接的情况,那么需要重新进行设计。它既简单又复杂。

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

但您必须认识到,即使这样做,您仍然无法测试两个线程相互作用的每个情况。要做到这一点,您必须在同一个测试中同时运行两个线程,然后精确地控制它们在任何给定时刻执行的行。你能做的最好的就是模拟这种情况。但这可能需要您专门为测试编写代码,这最多只需要向真正的解决方案迈出半步。

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

看,正如目前的情况(而且很可能是未来的好时机),测试多线程应用程序的最佳方法是尽可能降低线程代码的复杂性。最小化线程交互的区域,尽可能地进行测试,并使用代码分析来识别危险区域。


这个问题发表已经有一段时间了,但仍然没有得到回答…

克莱尔的回答很好。我将尝试更详细的内容。

有一种方法,我为C代码练习。对于单元测试,您应该能够编程可重复的测试,这是多线程代码中最大的挑战。所以我的答案是将异步代码强制到一个同步工作的测试工具中。

这是杰拉德·梅萨多斯的书《Xunit测试模式》中的一个想法,被称为"谦虚对象"(第695页):你必须将核心逻辑代码和任何类似异步代码的东西分开。这将导致核心逻辑的类同步工作。

这使您能够以同步方式测试核心逻辑代码。您可以完全控制对核心逻辑所做调用的时间,因此可以进行可重复的测试。这就是分离核心逻辑和异步逻辑的好处。

这个核心逻辑需要被另一个类包装,该类负责异步接收对核心逻辑的调用,并将这些调用委托给核心逻辑。生产代码将只通过该类访问核心逻辑。因为这个类应该只委托调用,所以它是一个非常"愚蠢"的类,没有太多逻辑。所以你可以把这个非对称的工人阶级的单元测试保持在最低限度。

上面的任何东西(测试类之间的交互)都是组件测试。在这种情况下,如果你坚持"卑微的对象"模式,你应该能够对时间有绝对的控制。


真的很难!在我的(C++)单元测试中,我根据使用的并发模式将其分解为几个类别:

  • 对在单个线程中运行且不了解线程的类进行单元测试——简单,像往常一样进行测试。

  • 公开同步公共API的监视对象(那些在调用方的控制线程中执行同步方法的对象)的单元测试——实例化多个模拟线程来运行该API。构建执行被动对象内部条件的场景。包括一个运行时间较长的测试,它基本上可以在很长一段时间内从多个线程中击败它。我知道这是不科学的,但它确实建立了信心。

  • 活动对象的单元测试(那些封装自己的线程或控制线程的对象)——类似于上面的2,根据类设计的不同而有所不同。公共API可能阻塞或不阻塞,调用方可能获得预购,数据可能到达队列或需要出列。这里有很多种可能的组合;白盒子。仍然需要多个模拟线程来调用被测对象。

  • 作为旁白:

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


    在最近几年为几个项目编写线程处理代码时,我已经多次遇到这个问题。我提供了一个延迟的答案,因为大多数其他的答案,在提供替代方案的同时,实际上并没有回答关于测试的问题。我的答案是针对没有多线程代码替代方案的情况;为了完整性,我确实讨论了代码设计问题,但也讨论了单元测试。好的。

    编写可测试的多线程代码好的。

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

    要记住的第二件事是多线程代码中的错误是概率性的;最不经常出现的错误是那些会潜入生产环境的错误,即使在生产环境中也很难重现,从而导致最大的问题。因此,对于多线程代码来说,快速地编写代码,然后对其进行调试的标准编码方法是一个坏主意;它将导致代码中容易修复的错误和危险的错误仍然存在。好的。

    相反,在编写多线程代码时,您必须以这样的态度编写代码,即首先要避免编写错误。如果您已经正确地删除了数据处理代码,那么线程处理代码应该足够小——最好是几行,最坏是几十行——这样您就有机会在不写bug的情况下编写它,当然也不会写很多bug,如果您了解线程,请慢慢来,并且要小心。好的。

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

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

    要正确测试正确的锁定行为,测试必须启动多个线程。为了使测试可重复,我们希望线程之间的交互以可预测的顺序发生。我们不希望在外部同步测试中的线程,因为这样会掩盖在没有外部同步线程的生产环境中可能发生的错误。这就剩下了线程同步时间延迟的使用,这是我在编写多线程代码测试时成功使用的技术。好的。

    如果延迟时间太短,则测试将变得脆弱,因为运行测试的不同机器之间的微小时间差可能会导致定时关闭和测试失败。我通常所做的是从导致测试失败的延迟开始,增加延迟以便测试在我的开发机器上可靠地通过,然后将延迟加倍,这样测试就有很好的机会通过其他机器。这确实意味着测试需要一个宏观的时间量,尽管根据我的经验,仔细的测试设计可以将时间限制在不超过12秒。由于您的应用程序中不应该有太多需要线程协调代码的地方,所以这对于您的测试套件来说应该是可以接受的。好的。

    最后,跟踪测试捕获的错误数量。如果您的测试有80%的代码覆盖率,那么它可以捕获大约80%的错误。如果您的测试设计良好,但没有发现错误,那么您就有可能没有只在生产中出现的其他错误。如果测试捕获了一两个bug,那么您可能仍然很幸运。除此之外,您可能还需要考虑对线程处理代码进行仔细的审查,甚至是对其进行完全重写,因为代码中可能仍然包含隐藏的错误,在代码投入生产之前很难找到这些错误,并且很难修复。好的。好啊。


    我在测试多线程代码时也遇到了严重的问题。然后我在杰拉德·梅萨罗斯的《Xunit测试模式》中找到了一个非常酷的解决方案。他所描述的模式被称为卑微的对象。

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


    周围有一些很好的工具。下面是一些Java的总结。

    一些好的静态分析工具包括FunBug(给出了一些有用的提示)、JLILT、Java PofFrfter(JPF和JPF2)和茂物。

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

    来自IBM Research的竞赛很有趣。它通过插入各种线程修改行为(如睡眠和收益)来检测代码,以尝试随机发现错误。

    SIP是一个非常酷的建模Java(和其他)组件的工具,但是你需要有一些有用的框架。它很难按原样使用,但如果你知道如何使用它,它就非常强大。相当多的工具在引擎盖下面使用旋转。

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


    另一种测试线程代码的方法,通常非常复杂的系统是通过模糊测试。它不太好,也找不到任何东西,但它可能很有用,而且很简单。

    报价:

    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.


    等待性还可以帮助您编写确定性单元测试。它允许您等待系统中的某个状态被更新。例如:

    1
    await().untilCall( to(myService).myMethod(), greaterThan(3) );

    1
    await().atMost(5,SECONDS).until(fieldIn(myObject).ofType(int.class), equalTo(1));

    它还支持scala和groovy。

    1
    await until { something() > 4 } // Scala example


    如前所述,测试mt代码的正确性是一个非常困难的问题。最后,归根结底就是要确保代码中没有不正确同步的数据竞争。这方面的问题在于,线程执行(交错)的可能性是无限多的,而您对此没有太多的控制权(不过,请务必阅读本文)。在简单的场景中,可能会通过推理实际证明正确性,但通常情况并非如此。尤其是如果您想避免/最小化同步,而不是选择最明显/最简单的同步选项。

    我遵循的一种方法是编写高度并发的测试代码,以使潜在的未检测到的数据竞争可能发生。然后我运行这些测试一段时间:)我有一次偶然发现一个谈话,在谈话中,一些计算机科学家展示了一种工具,这种工具可以做到这一点(从规范中随机设计测试,然后同时疯狂地运行它们,检查定义的不变量是否被破坏)。

    顺便说一下,我认为这里没有提到测试mt代码的这一方面:识别您可以随机检查的代码的不变量。不幸的是,找到这些不变量也是一个相当困难的问题。另外,它们在执行过程中可能不会一直保持不变,因此您必须在可以期望它们为真的地方找到/强制执行点。使代码执行达到这样的状态也是一个难题(并且本身可能会引发并发问题)。哎呀,这太难了!

    一些有趣的链接:

    • 确定性交错:允许强制某些线程交错,然后检查不变量的框架。
    • jmock-blitzer:压力测试同步
    • 断言并发:JUnit版本的压力测试同步化
    • 测试并发代码:对蛮力(压力测试)或确定性(不变量)两种主要方法的简要概述


    我做了很多,是的,很糟糕。

    一些提示:

    • 用于运行多个测试线程的groboutils
    • alphaWorks对工具类的竞争导致迭代之间的交错变化
    • 创建一个throwable字段,并在tearDown中检查它(参见清单1)。如果您在另一个线程中捕获了一个错误的异常,只需将其分配给throwable即可。
    • 我在清单2中创建了utils类,并发现它非常有用,尤其是waitforverify和waitforcondition,这将大大提高测试的性能。
    • 在测试中充分利用AtomicBoolean。它是线程安全的,通常需要一个最终的引用类型来存储回调类和类似的值。请参见清单3中的示例。
    • 确保总是给测试一个超时(例如,@Test(timeout=60*1000)),因为并发测试有时会在中断时永久挂起。

    清单1:

    1
    2
    3
    4
    5
    @After
    public void tearDown() {
        if ( throwable != null )
            throw throwable;
    }

    清单2:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    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 <wyn>null</wyn>
     * 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. <wyn>null</wyn> 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 <wyn>true</wyn> 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:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @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());
    }


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

    通过单元测试"证明"线程安全性要高得多。我相信,在各种平台/配置上进行自动化集成测试可以更好地实现这一点。


    PeteGoodliffe有一系列关于线程代码的单元测试。

    这很难。我采取了更简单的方法,并尝试将线程代码从实际测试中抽象出来。皮特确实提到我这样做是错误的,但我要么分居对了,要么我只是运气好而已。


    看看我的相关答案

    为自定义屏障设计测试类

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

    总之,虽然(IMO)并不是使用一些花哨的框架来确保正确性,而是如何设计多线程代码。分割关注点(并发性和功能性)有助于提高信心。由测试引导的面向对象软件的发展比我能更好地解释一些选项。

    静态分析和形式化方法(参见,并发:状态模型和Java程序)是一种选择,但我发现它们在商业开发中的应用很有限。

    别忘了,很少有人保证任何负载/浸泡式测试都能突出问题。

    祝你好运!


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

    因此,我编写了类似这样的包装(简化):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    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注入到组件中,并使用我选择的隔离框架使线程的行为与测试期间的预期一致。

    到目前为止,这对我来说非常有用,我对线程池、系统、环境、睡眠等使用相同的方法。


    我喜欢编写两个或更多的测试方法来在并行线程上执行,它们中的每一个都对被测试对象进行调用。我一直在使用sleep()调用来协调不同线程的调用顺序,但这并不可靠。它的速度也慢得多,因为你必须睡足够长的时间才能正常工作。

    我从编写FunBug的同一组中找到了多线程TC Java库。它允许您在不使用sleep()的情况下指定事件的顺序,并且它是可靠的。我还没试过。

    这种方法最大的限制是它只允许您测试您怀疑会导致问题的场景。正如其他人所说,您确实需要将多线程代码隔离到少数简单类中,以便有任何希望彻底测试它们。

    一旦你仔细测试了你期望会引起麻烦的场景,一个不科学的测试会在一段时间内同时向类中抛出一系列请求,这是寻找意想不到的麻烦的好方法。

    更新:我用多线程TC Java库玩了一点,效果很好。我还将它的一些特性移植到了我称之为TickingTest的.NET版本。


    我最近发现了(Java)一个叫做THealSead的工具。它是一个静态分析工具,与findbugs非常相似,但专门用于发现多线程问题。它不是测试的替代品,但我可以推荐它作为编写可靠的多线程Java的一部分。

    它甚至捕捉到一些非常微妙的潜在问题,比如类包容、通过并发类访问不安全的对象以及在使用双重检查锁定范式时发现丢失的易失性修饰符。

    如果你写多线程Java给它一个镜头。


    下面的文章提出了两种解决方案。包装信号量(countdownloatch)并添加诸如从内部线程外部化数据之类的功能。实现这一目的的另一种方法是使用线程池(请参见关注点)。

    喷水器-高级同步对象


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

    在编写测试时,我使用了委托和事件的组合。基本上,这都是关于使用PropertyNotifyChanged事件和WaitCallback或某种类型的ConditionalWaiter进行民意测验。

    我不确定这是否是最好的方法,但它已经为我解决了。


    上周的大部分时间我都在大学图书馆学习并行代码的调试。核心问题是并发代码是不确定性的。通常,学术调试在这里分为三个阵营:

  • 事件跟踪/重播。这需要一个事件监视器,然后检查发送的事件。在UT框架中,这将涉及手动发送事件作为测试的一部分,然后进行事后审查。
  • 可编写脚本的。这是使用一组触发器与正在运行的代码进行交互的地方。"在x>foo,baz()"上。这可以解释为一个UT框架,其中有一个运行时系统在特定条件下触发给定的测试。
  • 互动式。这显然在自动测试情况下不起作用。;)
  • 现在,正如上面的评论员注意到的,您可以将并发系统设计成一个更具确定性的状态。但是,如果你做得不好,你就重新开始设计一个顺序系统。

    我的建议是将重点放在有一个非常严格的设计协议,关于什么是线程化的,什么是不线程化的。如果您约束您的接口,使元素之间的依赖性最小,那么就容易多了。

    祝你好运,继续解决这个问题。


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

    • 状态的和可变的
    • 多线程访问/修改同时地

    换句话说,我们讨论的是测试定制的有状态线程安全类/方法/单元——这在当今应该是一种非常罕见的野兽。

    因为这种怪兽很罕见,首先我们需要确保有所有有效的借口来写它。

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

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

    这听起来可能非常有限,但这种方法非常有效。可以用这种方式编写整个应用程序,而无需使任何状态线程安全(我是这样做的)。

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

    确保车轮没有被重新改造/绝对没有标准的替代方案可以适应这项工作。代码很可能具有很强的内聚性并包含在一个单元中,例如,很有可能它是一些标准线程安全数据结构(如哈希图或集合等)的特例。

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

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

    我会非常诚实的:我从来没有必要为这样的代码编写适当的测试。大部分时间我都是在第一步,有时是在第二步。上一次我不得不编写自定义线程安全代码是在很多年前,那是在我采用单元测试之前/也许我不必用当前的知识来编写它。

    如果我真的需要测试这样的代码(最后,实际的答案),那么我会尝试下面的一些方法

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

  • 公开一些测试"钩子",在这些钩子中测试可以注入一些代码,以帮助进行确定性场景,其中一个线程必须先执行另一个线程的操作。虽然很难看,但我想不出比这更好的了。

  • 延迟驱动测试,使线程以特定的顺序运行和执行操作。严格地说,这种测试也是不确定性的(有可能系统冻结/停止世界GC集合,这会扭曲以其他方式安排的延迟),而且它很难看,但允许避免钩子。


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

    您可以在不安全的HTTP模式下发现一些错误,在这种模式下,您至少可以分析正在发送的表单数据,并为每个用户系统地更改这些数据。但真正的测试是在HTTPS(安全套接字层)中运行时进行的。然后,您还必须处理系统性地修改会话和cookie数据,这可能会更复杂一些。

    在测试并发时,我发现的最好的bug是当我发现开发人员依赖于Java垃圾收集来关闭登录时建立的连接请求时,登录到LDAP服务器。这导致用户暴露在其他用户的会话和非常混乱的结果中,当试图分析服务器被放到它的膝盖上时发生了什么,几乎不能每隔几秒钟完成一个事务。

    最后,您或其他人可能必须全力以赴分析代码中的错误,就像我刚才提到的那样。当我们展开上面描述的问题时,跨部门的公开讨论(如发生的讨论)是最有用的。但是这些工具是测试多线程代码的最佳解决方案。JMeter是开源的。SilkPerformer和LoadRunner是专有的。如果你真的想知道你的应用程序是否是线程安全的,那就是大男孩们的做法。我在专业上为大公司做过这件事,所以我没猜到。我是根据个人经验说的。

    注意:理解这些工具确实需要一些时间。这不是简单地安装软件和启动GUI的问题,除非您已经接触过多线程编程。我试图找出3个需要了解的关键领域(表单、会话和cookie数据),希望至少从了解这些主题开始,可以帮助您专注于快速结果,而不是必须阅读整个文档。


    并发性是内存模型、硬件、缓存和代码之间的复杂交互。在Java的情况下,至少这样的测试已经部分地以JC胁迫为主。该库的创建者是许多JVM、GC和Java并发特性的作者。

    但是,即使是这个库也需要对Java内存模型规范有很好的了解,以便我们能够准确地知道我们正在测试什么。但我认为这项工作的重点是Mircobenchmarks。不是巨大的商业应用。


    如果您正在测试简单的新线程(runnable).run()。您可以模拟线程来按顺序运行可运行的

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

    1
    2
    3
    4
    5
    Class TestedClass {
        public void doAsychOp() {
           new Thread(new myRunnable()).start();
        }
    }

    然后模拟新线程并按顺序运行可运行的参数可以帮助

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @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
    }

    (如果可能)不要使用线程,使用参与者/活动对象。易于测试。


    可以使用easymock.make threadsafe生成测试实例threadsafe