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

时间:2019-10-23 09:54:41

标签: java asynchronous mockito junit5 completable-future

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

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

这是我的方法的示例:

void testingMethod() {
    // some operations
    someObject.foo();
    CompletableFuture.runAsync(() -> {
        // some other operations
        someObject.foo();
    });
}

还有我的模拟someObject的测试示例:

@Test
public void testingMethodTest() {
    testObject.testingMethod();

    Mockito.verify(someObject, Mockito.times(2)).foo();
}

是否有一种方法可以在验证方法之前等待异步操作完成。还是这是一种不好的测试方法?在这种情况下,您能提供什么建议?

3 个答案:

答案 0 :(得分:1)

问题归结为以下事实:被测试的方法调用了静态方法:CompletableFuture.runAsync()。通常,静态方法几乎无法控制模拟和断言。

即使在测试中使用sleep(),也不能断言someObject.foo()是否被异步调用。如果在调用线程上进行了调用,则测试仍将通过。此外,使用sleep()会降低测试速度,而sleep()太短会导致测试随机失败。

如果确实这是唯一的解决方案,则应使用Awaitability之类的库,它会轮询直到断言得到满足,并且超时。

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

  1. 使testingMethod()返回一个Future (正如您在注释中所想到的那样):这不允许断言异步执行,但可以避免等待过多;
  2. runAsync()方法包装到另一个可以模拟的服务中,并捕获参数;
  3. 如果您使用的是Spring ,请使用@Async注释的方法将lambda表达式移动到另一个服务。这样可以轻松模拟和对该服务进行单元测试,并消除直接调用runAsync()的负担;
  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);
    }
}

实现自定义Executor,该自定义class CapturingExecutor implements Executor { private Runnable command; @Override public void execute(Runnable command) { this.command = command; } public Runnable getCommand() { return command; } } 只是捕获命令而不是运行命令:

@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(); } }

PowerRun_x64.exe "Reg.exe" delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows Defender\Testing" /f

答案 1 :(得分:0)

在测试中,TimeUnit.SECONDS.sleep(1);之后可以有一个testObject.testingMethod();

从侧面说,我什至不认为您应该测试异步发生的事情是否已完成或被调用,或者它对该功能不负责。

答案 2 :(得分:0)

对于那些正在本主题中寻求更多建议的人,请尝试使用Mockito.timeout()函数,因为该函数专门用于测试异步方法。

示例:

verify(mockedObject,timeout(100).times(1)).yourMethod();

还有另一种称为after()的方法。这也很有用。

https://www.javadoc.io/doc/org.mockito/mockito-core/2.2.9/org/mockito/verification/VerificationWithTimeout.html