关于在JUnit Test中使用多线程执行任务莫名退出的问题

前言

在开发Springboot 项目 实现一些功能时, 习惯性的Generate Test…

然后简单测试一下代码能否执行成功, 有无bug, 以及一些逻辑是否与需求一致…balabala

然后遇到了这个问题…

遇到的问题…

像往常一样, 创建Test去调试某个方法, 传参漏几个, 参数不存在等条件都验证通过了.

然后因为这个功能是作为异步执行. 需要给它加上@Async (以及启动类上加@EnableAsync) 注解, 在加上之后, 发现这个方法每次都执行到mapper查数据哪一行, 就莫名其妙退出了, 因为在方法结尾有打印输出一些乱七八糟的标识符, 但console中没有打印. 而且也没有任何报错or异常…在debug下发现每次都执行到mapper查sql那条语句时就退出…

在离谱的路上越走越远…

一开始以为是mapper没有成功注入? 但debug时显示mapper是有实例的, 不是null

  1. 在mapper前面写上一句从ApplicationContext中获取DataSource, 显示DataSource也是有的.

  2. Idea Debug 时, 对当前断点行 Evaluate Expression, 也是能拿到结果的…

  3. 难道是注解的问题? (之前是直接用线程池调, 没用过注解) 然后网上搜了一波教程, 看了一下感觉也没啥问题.

  4. 去掉注解再次执行. 正常执行完成, 这一下把问题定到注解上了…(当事人表示非常后悔)

  5. 在去掉注解后, 在调用方法的后面又通过new Thread的方式再次调了一遍方法, 第一遍(直接执行)正常执行完成, 第二遍(新建线程执行)又是执行到查sql那里…

  6. 因为项目有配异常拦截器. 以为是不是出了什么异常(虽然参数什么的完全一样), 然后被拦截器吞了? 又在 mapper 这里加上 try catch块打印一下, 还是像之前一样, 没有任何异常报错, 又是在mapper 这里, 莫名其妙的退出了…

回到正轨…

  1. (这里应该是脑子抽了一下) 把直接调用方法执行的语句放到了线程调用语句的后面, 然后就成功了! 两遍方法成功都把方法最后的标识符打印出来了!

  2. 再把顺序调回去试试, 又不行了..就大概猜到和线程执行有关

  3. 然后在线程执行的后面, 主任务上加上一个 Thread.sleep(1000), 试了几次, 有的成功有的失败(有的打印两遍, 有的打印一遍). 调高sleep时间到5000ms 成功.

有(qi)用(guai)的知识增加了…

在上面回到正轨后, 大概猜到了原因, 经搜索相关问题并得到以下内容

JUnit会在主线程直接完成后调用System.exit退出

猜测

JUnit Test创建了一个新进程执行测试方法, 通过jps命令可以查看当前Java进程, 很明显可以发现当测试用例执行时, 执行jps 会看到一个新的进程

猜测. 执行测试用例时, 会新建一个进程, 主线程启动Spring容器, 加载程序运行需要的一些环境配置, 实例等信息 (前期准备工作) , 然后会执行测试函数中的内容, 当执行完成时, 主线程会调用System.exit退出

(具体实现过程待确认)

所以测试方法中, 新建的线程在执行过程中会因为进程退出而结束, 而因为方法中前面是几个简单的处理逻辑, 执行会很快, 可能进程还没结束就执行过去了, 而mapper读取数据会比较耗时, 所以每次进程退出时, 线程都是执行到这个位置, 给人已在这里异常结束的假象.

测试函数主流程加上sleep(1000), 就有玄学的成分了, 相当于两者竞争, 到底是mapper那边读数据快, 还是sleep + 进程结束更快, 也就有了部分成功, 部分失败的情况

Test 的额外知识点

对于一个TestClass中的多个@Test方法. 直接执行TestClass时, 所有的@Test方法会在一个线程中线性执行, 但并没有一个具体顺序, 可以通过@Rule注解 按顺序读取

退出程序的一种可能方法是简单地到达main方法的末尾。但是如果有任何非守护程序线程在运行, 程序就不会关闭, 所以JUnit Test在主方法执行完成后, 调用System.exit()进行退出

System.exit() 会执行数据保存, 资源关闭等工作, 目的是为了在JVM关闭之前执行一些收尾的动作

参考: Thread behaving strangely in JUnit