我正在我的一个服务方法中调用
@Async
方法,现在我尝试模拟 @Async
方法在后台抛出异常的情况,以断言调用者方法继续正常执行,尽管异步抛出异常。
最初在测试中,我尝试模拟抛出的异常:
when(myAsyncServiceMock.myAsyncMethod(...some arguments...).thenThrow(new RuntimeException("failed to complete asynchronous method"));
但在这种情况下,调用者方法的行为就像在主线程中抛出运行时异常,而不是异步抛出。
Mockito 是否有办法指定
thenThrow(...)
中的异常应该异步抛出,而不是在主线程中?
@Async
是一个Spring框架注释 - 如果方法调用是在Spring上下文中(Spring bean之间)执行的,则该方法可能会被拦截并在与原始调用者的线程不同的线程上运行。如果在 Spring 上下文之外调用该方法,则忽略 @Async
注释,并且该方法将在与调用者相同的线程上执行。
您可以编写一个涉及 Spring 的测试并验证如下行为:
@SpringBootTest
class TestedClassSpringTest {
@MockBean
SomeService someService;
@Autowired
TestedClass testedClass;
@Test
void test() {
var exception = new RuntimeException("test error");
when(someService.doStuff())
.thenThrow(exception);
ThrowingCallable methodCall = () -> testedClass.execute();
assertThatNoException()
.isThrownBy(methodCall);
}
}
您将在控制台中看到异常实际上被抛出并记录,但原始调用并没有因为异常而中断,因为它在另一个线程上运行。如果需要,您还可以验证是否调用了
AsyncUncaughtExceptionHandler
(使用 Mockito.verify
方法),但在这种情况下,您必须记住调用是在与主线程不同的线程上执行的,因此使用可能需要诸如Awaitility之类的库来避免片状测试(主线程可能在异步线程之前完成运行)。
注意:assertThatNoException
是来自AssertJ库的方法。
在测试中模拟异步执行的另一种方法可以简单地自己创建新线程,模仿 Spring 行为:
class TestedClassVanillaTest {
SomeService someService = mock(SomeService.class);
TestedClass testedClass = new TestedClass(someService);
@Test
void test() {
var exception = new RuntimeException("test error");
when(someService.doStuff())
.thenThrow(exception);
ThrowableAssert.ThrowingCallable methodCall = () -> {
new Thread(() -> testedClass.execute())
.start();
};
assertThatNoException()
.isThrownBy(methodCall);
}
}
这种方法的缺点是,不需要
@Async
注释即可工作 - 如果将来将其删除,测试仍然会通过。我建议不要使用这种方法,因为它只是测试一个事实:在不同线程中抛出的异常不会被主线程捕获,这是 Java 的老生常谈。尽管如此 - 这些测试仍然有助于理解 Spring 如何“在幕后”做事。
我已经准备了一个 GitHub 存储库 - 我验证了这两个测试并且它们通过了。