Junit单元测试不支持多线程
1. “缘”起
在回顾synchronized的用法时,写了一个小程序,就是使用synchronized修饰一个代码块。核心代码如下所示:
然后,使用Junit进行单元测试,测试代码如下:
运行结果如下:
显然,这和预期的结果是不一样的。然后,我又在java的main方法里写了同样的测试语句,运行结果如下:
那么,为什么使用Junit运行出来的结果和main方法里面运行的结果不一样呢?我试着进行调试,中间具体的代码也没太细看,就关注了一下结果,发现有时候Junit也能输出完整的预期结果,这是为什么呢?
2. 浅知
上网搜查了一下,看到的解释是Junit单元测试不支持多线程。很多都说是通过看TestRunner源码知道,测试主程序结束之后,会调用System.exit()方法关闭程序,结束当前正在运行的虚拟机。群里的伙伴说,没有守护线程,应该是说Junit没有守护线程吧。但是main方法有守护线程吗?怎么知道有没有守护线程的呢?
继续搜索,在一篇博客(http://www.voidcn.com/article/p-brbjzhux-np.html)中写道“Junit会把第一个启动的线程作为守护线程,当守护线程结束后程序也就终止了,而main方法启动的时候本身线程会作为守护线程存在,等待其他线程执行结束”。但是,感觉这解释也是有点自相矛盾。
后来,群里伙伴又提到使用线程池的话,可以在单元测试里运行完。这个后面再说吧。
3. 解惑
在很多博客中看到,使用Junit的@Test注解的方法,最终是由TestRunner里面的main方法执行的。Main方法里面的参数args就是被注解的方法,参数如下图所示:
然后,我们看一下TestRunner里的main方法,被注解的方法是由TestRunner里面的start方法执行。一旦被注解的方法执行完(只关注当前方法的执行语句,不关注方法中其他线程是否执行完,其实想想异步也就清楚了),无论方法执行成功或是失败,都会调用System.exit()方法,结束当前正在运行的虚拟机。因此,就会导致在Junit方法执行的测试测试语句达不到预期的效果。其实,如果你在main方法里面加上System.exit()语句后,也达不到预期的效果,不信可以试一试。
所以,问题的根本原因不是说没有守护线程,因为都是main这个用户线程在执行。只是一个执行System.exit()语句关闭了虚拟机,一个没有关闭虚拟机。同理,使用线程池的话,也是不能执行出预期的结果,在此就不在赘述了。
重点来了……
虽然找到了TestRunner方法,但是,却找不到程序从什么地方进入的。然后,看了汪洋师兄(他的主页:https://blog.csdn.net/w605283073/article/details/92016433)的博客后,我觉得还是要好好看一下调用栈。截图如下:
如红色方框所示,程序进入是从idea的启动程序中进入的,之前我以为是从idea启动,然后中间会通过某种方法执行到TestRunner,后来发现还真不是。然后,从idea的安装目录中(我的是在D:\development\idea\plugins\junit\lib),找到了junit-rt.jar,然后查看该jar包发现,程序的流程是下面这样的:
(1)首先,进入到com.intellij.rt.execution.junit.JUnitStart中的main方法,由prepareStreamsAndStart函数来执行。
(2)再看一下prepareStreamsAndStart函数,如下:
程序的返回是从这里开始的,最终返回的结果是由我们添加的Junit的jar包程序JunitCore.run决定的,代码如下,而不是由TestRunner决定的。
别的代码在此就不细讲了,可以自己看一下调用栈就清楚了。虽然Junit不支持多线程是由于程序提前退出虚拟机决定的,但是,测试的方法不是由Junit里的TestRunner的main方法执行的。
4. 使用
如果项目中必须要在单元测试里执行多线程方法的话,怎么办呢,可参考:
https://blog.csdn.net/fcs_learner/article/details/81056868。
(1)thread.join()
(2)CountDownLatch
(3)CyclicBarrier