Junit中的断言在线程中抛出异常

30

我做错了什么导致抛出异常而不是显示失败,或者我不应该在线程内使用断言吗?

 @Test
 public void testComplex() throws InterruptedException {
  int loops = 10;
  for (int i = 0; i < loops; i++) {
   final int j = i;
   new Thread() {
    @Override
    public void run() {
     ApiProxy.setEnvironmentForCurrentThread(env);//ignore this
     new CounterFactory().getCounter("test").increment();//ignore this too
     int count2 = new CounterFactory().getCounter("test").getCount();//ignore
     assertEquals(j, count2);//here be exceptions thrown. this is line 75
    }
   }.start();
  }
  Thread.sleep(5 * 1000);
  assertEquals(loops, new CounterFactory().getCounter("test").getCount());
}

堆栈跟踪

Exception in thread "Thread-26" junit.framework.AssertionFailedError: expected:<5> but was:<6>
    at junit.framework.Assert.fail(Assert.java:47)
    at junit.framework.Assert.failNotEquals(Assert.java:277)
    at junit.framework.Assert.assertEquals(Assert.java:64)
    at junit.framework.Assert.assertEquals(Assert.java:195)
    at junit.framework.Assert.assertEquals(Assert.java:201)
    at com.bitdual.server.dao.ShardedCounterTest$3.run(ShardedCounterTest.java:77)

你为什么在这个测试中创建一个新的线程?我的意思是,你为什么想在单元测试中创建线程? - Cem Catikkas
@Cem,我有一组正在开发中的(初始)测试用例,其中一个测试用例(尝试)是为了检测竞态条件(我说要忽略的3行代码在这个讨论中变得相关)。有没有更好的方法来进行竞态条件测试?我需要转向另一个工具来进行这种类型的测试吗? - antony.trupe
你不能通过单元测试来真正测试竞态条件,特别是通过创建线程来模拟情况。即使在你提供的这个例子中,你正在检查当第二个线程运行时计数器应该为2。即使你按顺序创建线程,它们也不一定会按相同的顺序运行。此外,在你调用增量和获取之间,线程可能会被抢占,因此你的测试中已经存在竞态条件。偶尔会通过或失败。单元测试应该更加确定性。 - Cem Catikkas
3
虽然这些特定的断言有点天真,但是对于我的具体情况,单元测试非常有效地捕捉到了大部分竞争/争用问题。 - antony.trupe
6个回答

46
JUnit框架只捕获运行测试的主线程中的断言错误,不知道来自新线程内部的异常。为了正确地处理此问题,应该将子线程的终止状态通知主线程。必须正确同步线程,并使用某种共享变量来指示嵌套线程的结果。 编辑:以下是可行的通用解决方案:
class AsynchTester{
    private Thread thread;
    private AssertionError exc; 

    public AsynchTester(final Runnable runnable){
        thread = new Thread(() ->
            {
                try{            
                    runnable.run();
                }catch(AssertionError e) {
                    exc = e;
                }
            }
        );
    }
    
    public void start(){
        thread.start();
    }
    
    public void test() throws InterruptedException {
        thread.join();
        if (exc != null)
            throw exc;
    }
}

在构造函数中传递可运行对象,然后只需调用start()来激活并调用test()进行验证。如果需要,test()方法将等待,并在主线程的上下文中抛出断言错误。


3
在这个例子中,你应该正确地同步线程...简单的方法是让主线程对子线程调用join(),并且去掉sleep(5000)的调用。 - Stephen C
睡眠调用有点怪味,但因为这是单元测试代码,我没有过多纠结,不过现在我知道了,我肯定会使用正确的方法。 - antony.trupe
1
顺便提一下,exc 不需要是 volatile 的,因为 Thread.join() 会将被 join 的线程的状态与主线程同步。 - eregon
1
@eregon:你是对的。这个数据成员的发生顺序关系已经很好地建立了。已修复。 - Eyal Schneider

19

Eyal Schneider的回答进行了一点小改进:
ExecutorService允许提交一个Callable,任何抛出的异常或错误都将由返回的Future重新抛出。
因此,测试可以编写为:

@Test
public void test() throws Exception {
  ExecutorService es = Executors.newSingleThreadExecutor();
  Future<?> future = es.submit(() -> {
    testSomethingThatMightThrowAssertionErrors();
    return null;
  });

  future.get(); // This will rethrow Exceptions and Errors as ExecutionException
}

6

当涉及到多个工作线程时,例如在原始问题中,仅加入其中一个线程是不够的。理想情况下,您将希望等待所有工作线程完成,同时仍然向主线程报告断言失败,例如 Eyal 的答案。

以下是使用我的 ConcurrentUnit 进行此操作的简单示例:

public class MyTest extends ConcurrentTestCase {
    @Test
    public void testComplex() throws Throwable {
        int loops = 10;
        for (int i = 0; i < loops; i++) {
            new Thread(new Runnable() {
                public void run() {
                    threadAssertEquals(1, 1);
                    resume();
                }
            }).start();
        }

        threadWait(100, loops); // Wait for 10 resume calls
    }
}

1
JUnit会抛出继承自Throwable的AssertionError,它的父类与Exception相同。您可以捕获线程中失败的断言,然后将其保存在静态字段中,最后在主线程中检查其他线程是否已经失败了某些断言。
首先,创建静态字段。
private volatile static Throwable excepcionTE = null;

其次,将断言语句放入try/catch块中,并捕获AssertionError异常。

        try
    {
      assertTrue("", mensaje.contains("1234"));
    }
    catch (AssertionError e)
    {
      excepcionTE = e;
      throw e;
    }

最后,在主线程中检查该字段。
 if (excepcionTE != null)
{
  excepcionTE.printStackTrace();
  fail("Se ha producido una excepcion en el servidor TE: "
      + excepcionTE.getMessage());
}

0

我最终使用了这个模式,它适用于Runnables和Threads。它在很大程度上受到@Eyal Schneider的答案的启发:

private final class ThreadUnderTestWrapper extends ThreadUnderTest {
    private Exception ex;

    @Override
    public void run() {
        try {
            super.run();
        } catch ( Exception ex ) {
            this.ex = ex;
        }
    }

    public Exception getException() throws InterruptedException {
        super.join(); // use runner.join here if you use a runnable. 
        return ex;
    }
}

0

我正在寻找一个简单易读的解决方案。受 Eyal Schneider 和 Riki Gomez 答案的启发,我想出了这个:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;


public class ThreadExceptionTest {
    private static Throwable failedThreadException;

    @BeforeEach
    public void setup() {
        failedThreadException = null;
    }

    @Test
    public void threadTest() {
        final Thread thread = new Thread(() -> codeThatMayFail(...));

        thread.start();

        // We have to join before we check for exceptions, 
        //   otherwise we might check before the Thread even finished.
        thread.join();

        if (failedThreadException != null) {
            fail("The thread failed with an exception", failedThreadException);
        }
    }

    private void codeThatMayFail(...) {
        try {
            // Code that may throw the exception
            // ...
        } catch (Exception e) {
            failedThreadException = e;
        }
    }
}

所以,你可以通过使用 static 变量来实现所需的结果。线程按照通常的方式运行,而你要做的就是将所感兴趣的异常存储在 static 变量中。只需要不要忘记在每次测试之前将其值重置为 null,否则可能会在同一类的后续测试中遇到麻烦。

最后说明:如果你计划在同一测试上运行多个线程,并且预期它们同时运行相同的代码块,我建议将 static 变量设置为 volatile,以便对变量的更新可预测地传播到其他线程:

private volatile static Throwable failedThreadException;

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接