本博客站点已全量迁移至 DevDengChao 的博客 https://blog.dengchao.fun , 后续的新内容将优先在自建博客站进行发布, 欢迎大家访问.
文章目录
简介
近期公司里有一些业务需要服务端异步执行, 于是开发团队自热而然的使用了 Spring 自带的 @Async
注解标记了业务方法, 并使用 @EnableAsync
注解启用了异步功能, 但是编写单元测试时却遇到了由于异步引发的问题: 测试用例线程与业务线程异步, 导致无法正确的对业务进行断言.
于是我们尝试了多种不同的方式来对异步业务进行调试与测试, 最终得出了以下几种测试方式, 为了方便演示, 以如下 AsyncService
为例进行介绍:
@Service
class AsyncService { // 为了简洁省去部分访问修饰符
boolean done = false;
@Async
void myAsyncMethod() {
Thread.sleep(1000); // 模拟 1 秒钟的业务耗时
done = true; // 模拟异步方法造成的业务影响
}
}
方式一: 阻塞测试用例线程
思路非常简单: 既然测试用例线程比业务线程要提前结束, 那直接阻塞测试用例线程, 让它跑得比业务线程慢不就行了.
@SpringBootTest
class BlockUnitTestThreadAsyncServceTest {
@Autowired
AsyncService service;
@Test
void test() {
service.myAsyncMethod();
// 强制测试用例线程等待一定时间后再进行断言
Thread.sleep(2000);
assertTrue(service.done);
}
}
可以看出这种方式比较笨拙 🤦♂️, 因为业务耗时一旦变长, 测试用例随时可能会断言失败, 而且如果业务耗时显著小于测试用例中等待的时间, 则整体的测试耗时又会被不必要的等待拉长.
方式二: 分离异步线程与业务逻辑
思路: 既然异步不好测试, 那直接把业务拆出来, 同步测试业务不就行了.
于是新增一个 SyncService
:
@Service
class SyncService {
boolean done = false;
void mySyncMethod() {
Thread.sleep(1000); // 模拟 1 秒钟的业务耗时
done = true; // 模拟业务影响
}
}
同时改造 AsyncService
:
@Service
class AsyncService { // 使用 AsyncService 套壳
@Autowired
SyncService service;
@Async
void myAsyncMethod() {
service.mySyncMethod();
}
}
编写测试用例:
@SpringBootTest
class SyncServceTest {
@Autowired
SyncService service;
@Test
void test() {
service.mySyncMethod();
// 由于是同步执行, 因此与常规 Service 对象的测试方式没有什么差别
assertTrue(service.done);
}
}
这种方式需要对业务代码进行一定量的修改, 而且随着需要异步执行的业务增加, 势必会出现越来越多的套壳类/方法. 同时, 由于 AsyncService
的存在, 会出现缺少测试用例导致整体测试覆盖率下降, 或出现仅调用 AsyncService
方法但不进行断言的无实际意义的 AsyncServiceTest
的情况.
方式三: 偷梁换柱, 替换测试用例的 Executor
由于上述两种方式都存在一定的缺陷, 因此我们不得不进一步的研究文档和源码, 以寻求更合适的测试方式.
查看 Spring 官方提供的引导教程 Creating Asynchronous Methods https://spring.io/guides/gs/async-method/ 后发现, 这篇文章中仅介绍了如何开启异步功能并如何使用 @Async
注解, 但是完全没有介绍如何对其进行测试. 前往其对应的仓库 https://github.com/spring-guides/gs-async-method , 也没有发现测试相关的内容. 😢
考虑到平时学习异步相关的内容时都会牵扯到 Executor
, 因此以它为切入点, 想办法把测试用例中的异步执行器替换成同步执行器, 但是怎么去替换, 大家都不懂, 看来只能研究源码了 🕵️♂️.