问题归结为测试方法调用静态方法:CompletableFuture.runAsync()。静态方法通常对模拟和断言几乎没有控制。
即使您在测试中使用sleep(),您也无法断言someObject.foo() 是否被异步调用。如果调用是在调用线程上进行的,测试仍然会通过。此外,使用sleep() 会减慢您的测试速度,而太短的sleep() 会导致测试随机失败。
如果这似乎是唯一的解决方案,您应该使用像 Awaitability 这样的库来轮询直到满足断言,并超时。
有几种方法可以让您的代码更易于测试:
-
使
testingMethod() 返回一个Future(正如您在 cmets 中所想的那样):这不允许断言异步执行,但它避免了太多等待;
-
将
runAsync() 方法包装在您可以模拟的另一个服务中,并捕获参数;
-
如果您使用的是 Spring,请将 lambda 表达式移动到另一个服务中,并使用带有
@Async 注释的方法。这允许轻松地模拟和单元测试该服务,并消除直接调用runAsync() 的负担;
-
使用自定义执行器,并将其传递给
runAsync();
如果您使用的是 Spring,我建议您使用第三种解决方案,因为它确实是最干净的,并且可以避免到处都有 runAsync() 调用使您的代码混乱。
选项 2 和 4 非常相似,只是改变了您必须模拟的内容。
如果您选择第四种解决方案,您可以这样做:
更改测试类以使用自定义Executor:
class TestedObject {
private SomeObject someObject;
private Executor executor;
public TestedObject(SomeObject someObject, Executor executor) {
this.someObject = someObject;
this.executor = executor;
}
void testingMethod() {
// some operations
someObject.foo();
CompletableFuture.runAsync(() -> {
// some other operations
someObject.foo();
}, executor);
}
}
实现一个自定义Executor,它只是捕获命令而不是运行它:
class CapturingExecutor implements Executor {
private Runnable command;
@Override
public void execute(Runnable command) {
this.command = command;
}
public Runnable getCommand() {
return command;
}
}
(你也可以@MockExecutor 并使用ArgumentCaptor,但我认为这种方法更简洁)
在您的测试中使用CapturingExecutor:
@RunWith(MockitoJUnitRunner.class)
public class TestedObjectTest {
@Mock
private SomeObject someObject;
private CapturingExecutor executor;
private TestedObject testObject;
@Before
public void before() {
executor = new CapturingExecutor();
testObject = new TestedObject(someObject, executor);
}
@Test
public void testingMethodTest() {
testObject.testingMethod();
verify(someObject).foo();
// make sure that we actually captured some command
assertNotNull(executor.getCommand());
// now actually run the command and check that it does what it is expected to do
executor.getCommand().run();
// Mockito still counts the previous call, hence the times(2).
// Not relevant if the lambda actually calls a different method.
verify(someObject, times(2)).foo();
}
}