如何使用线程执行单元测试?

时间:2008-11-24 16:02:09

标签: java multithreading unit-testing junit

执行摘要:当线程中抛出断言错误时,单元测试不会消失。这是有道理的,因为不应该允许一个线程崩溃另一个线程。问题是我如何1)在第一个辅助线程崩溃时使整个测试失败,或者2)循环并确定每个线程完成后的状态(参见下面的代码)。执行后者的一种方法是通过具有每线程状态变量,例如“boolean [] status”并且具有“statuses [i] == false”意味着线程失败(这可以被扩展以捕获更多信息)。但是,这不是我想要的:我希望它在抛出断言错误时就像任何其他单元测试一样失败。这甚至可能吗?这是可取的吗?

我感到无聊,我决定在我的单元测试中产生一堆线程,然后让他们调用一个服务方法,只是为了它。代码看起来大致如下:

Thread[] threads = new Thread[MAX_THREADS];
for( int i = 0; i < threads.length; i++ ) {
    threads[i] = new Thread( new Runnable() {
        private final int ID = threadIdSequenceNumber++;
        public void run() {
            try {
                resultRefs[ID] = runTest( Integer.toString( ID ) ); // returns an object
            }
            catch( Throwable t ) { 
                // this code is EVIL - it catches even
                // Errors - don't copy it - more on this below
                final String message = "error testing thread with id => "
                            + ID;
                logger.debug( message, t );
                throw new IllegalStateException( message, t ); 
                // need to wrap throwable in a 
                // run time exception so it will compile
            }
        }
    } );
}

在此之后,我们将循环遍历线程数组并启动每个线程。之后我们将等待他们全部完成。最后,我们将对结果引用进行一些检查。

for( Thread thread : threads )
    thread.start();

logger.debug( "waiting for threads to finish ..." );
boolean done = false;
while( !done ) {
    done = true;
    for( Thread thread : threads )
        if( thread.isAlive() )
            done = false;
}

for( int i = 0; i < resultRefs.length; i++ ) {
    assertTrue( "you've got the world messed, dawg!",
            myCondition(resultRefs[i]) );

这是问题所在。你有没有注意到那个讨厌的try-catch-throwable块?我刚刚补充说,作为临时黑客,所以我可以看到发生了什么。在runTest(String)中,会产生一些断言,例如assertNotNull(null),但由于它位于不同的线程中,因此不会导致单元测试失败!!!!

我的猜测是我们需要以某种方式迭代线程数组,检查每个数据的状态,并在线程以令人讨厌的方式终止时手动导致断言错误。提供此信息的方法的名称是什么(死线程的堆栈跟踪)。

7 个答案:

答案 0 :(得分:8)

并发性是单元测试非常困难的事情之一。如果您只是想测试每个线程中的代码是否正在执行它应该测试的内容,那么您可能应该只测试上下文中隔离的代码。  如果在此示例中线程协作以达到结果,则可能是您可以在不使用线程的情况下测试该协作。这将通过顺序执行所有协作部分来完成。  如果你想测试竞争条件和这些东西,单元测试不是最好的方法。您将获得有时会失败但有时不会失败的测试。  总而言之,我认为可能是你的问题是你的单元测试水平太高了。  希望这有帮助

答案 1 :(得分:5)

Google测试博客上有一篇关于此主题的优秀文章非常值得一读:http://googletesting.blogspot.com/2008/08/tott-sleeping-synchronization.html

它是用Python编写的,但我认为这些原则可直接转移到Java。

答案 2 :(得分:2)

在多线程环境中进行单元测试非常困难......因此需要进行一些调整。单元测试必须是可重复的。确定性的。因此,具有多个线程的任何内容都不符合此条件。多线程测试也往往很慢。

  • 我要么试着看看我是否可以在单个线程上进行测试..被测逻辑是否真的需要多个线程。
  • 如果这不起作用,请使用成员变量方法,当所有线程都已完成运行时,您可以在测试结束时检查预期值。
嘿,好像还有另一个问题就像这样。查看我的帖子,找到tdd yahoogroup上更长时间讨论的链接 Unit testing a multithreaded application?

答案 3 :(得分:1)

您的runnable包装器应该将异常对象传递回您的测试类,然后您可以将它们存储在一个集合中。完成所有测试后,您可以测试该集合。如果它不为空,则遍历每个异常和.printStackTrace()然后失败。

答案 4 :(得分:1)

实现UncaughtExceptionHandler设置一些标志(线程进行周边检查)和set it on each Thread

答案 5 :(得分:0)

Junit并发线程测试的另一个流行选项是使用自定义JunitRunner和简单注释的Matthieu Carbou方法。

See the full documentation

答案 6 :(得分:0)

通过使用特殊的同步对象,可以使单元测试失败。看看下面的文章: Sprinkler - Advanced synchronization object

我将尝试解释这里的要点。 您希望能够将内部线程故障外部化到主线程,在您的情况下是测试。因此,您必须使用内部线程和测试将用于彼此同步的共享对象/锁。 请参阅以下测试 - 它通过调用名为Sprinkler的共享对象创建一个模拟抛出异常的线程。 主线程(测试)在Sprinkler.getInstance()上被阻止.await(CONTEXT,10000) 当被释放时,它将被释放并捕获引发的异常。 在catch块中,您可以编写未通过测试的断言。

 @Test
    public void testAwait_InnerThreadExternalizeException() {

        final int CONTEXT = 1;
        final String EXCEPTION_MESSAGE = "test inner thread exception message";

        // release will occur sometime in the future - simulate exception in the releaser thread
        ExecutorServiceFactory.getCachedThreadPoolExecutor().submit(new Callable<void>() {

            @Override
            public Void call() throws Exception {

                Sprinkler.getInstance().release(CONTEXT, new RuntimeException(EXCEPTION_MESSAGE));

                return null;
            }

        });

        Throwable thrown = null;
        try {
            Sprinkler.getInstance().await(CONTEXT, 10000);
        } catch (Throwable t) {
            // if the releaser thread delivers exception it will be externelized to this thread
            thrown = t;
        }
        Assert.assertTrue(thrown instanceof SprinklerException);
        Assert.assertEquals(EXCEPTION_MESSAGE, thrown.getCause().getMessage());
    }