如何在单元测试中测试异步操作内的方法调用

Sil*_*ird 3 java asynchronous mockito completable-future junit5

我有一个方法,它首先执行一系列操作,然后启动异步任务。我想测试这个方法,但我不明白如何验证异步操作是否已完成。

\n\n

使用 Mo\xd1\x81kito,我想验证foo方法是否执行了两次,一次是在异步任务开始之前,一次是在异步任务内部。问题是,在 Mockito 检查时,异步任务可能尚未调用异步操作内的方法。因此,有时进行测试,有时不进行测试。

\n\n

这是我的方法的示例:

\n\n
void testingMethod() {\n    // some operations\n    someObject.foo();\n    CompletableFuture.runAsync(() -> {\n        // some other operations\n        someObject.foo();\n    });\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

我的测试示例,其中 someObject 被嘲笑:

\n\n
@Test\npublic void testingMethodTest() {\n    testObject.testingMethod();\n\n    Mockito.verify(someObject, Mockito.times(2)).foo();\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

有没有办法在验证方法之前等待异步操作完成。或者这是一种不好的测试方法,在这种情况下您有什么建议?

\n

Did*_*r L 5

问题归结为这样一个事实:被测试的方法调用静态方法:CompletableFuture.runAsync()。一般来说,静态方法对模拟和断言几乎没有控制。

即使您sleep()在测试中使用 a,也无法断言是否someObject.foo()异步调用。如果在调用线程上进行调用,测试仍然会通过。此外,使用sleep()会减慢你的测试速度,太短sleep()会导致测试随机失败。

如果这确实是唯一的解决方案,那么您应该使用像Awaitability这样的库,它会轮询直到满足断言,并设置超时。

有几种替代方法可以使您的代码更易于测试:

  1. 使testingMethod()return aFuture(正如您在评论中所想的那样):这不允许断言异步执行,但可以避免等待太多;
  2. runAsync()方法包装在您可以模拟的另一个服务中,并捕获参数;
  3. 如果您使用 Spring,请将 lambda 表达式移动到另一个服务,在用 注释的方法中@AsyncrunAsync()这允许轻松地模拟和单元测试该服务,并消除直接调用的负担;
  4. 使用自定义执行器,并将其传递给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);
    }
}
Run Code Online (Sandbox Code Playgroud)

实现一个Executor仅捕获命令而不是运行命令的自定义:

class CapturingExecutor implements Executor {

    private Runnable command;

    @Override
    public void execute(Runnable command) {
        this.command = command;
    }

    public Runnable getCommand() {
        return command;
    }
}
Run Code Online (Sandbox Code Playgroud)

(您也可以@Mock使用Executor并使用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();
    }
}
Run Code Online (Sandbox Code Playgroud)