繁体   English   中英

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

[英]How can I test a method call inside an asynchronous operation in unit testing

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

使用 Moсkito,我想验证foo方法是否被执行了 2 次,一次在异步任务开始之前,一次在其中。 问题是在 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();
}

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

对于那些您在本主题中寻找更多建议的人,请尝试Mockito.timeout() function,因为它专门用于测试异步方法。

例子:

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

问题归结为测试方法调用了 static 方法: CompletableFuture.runAsync() Static 方法通常对 mocking 和断言几乎没有控制。

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

如果这似乎是唯一的解决方案,您应该使用像Awaitability这样的库来轮询直到断言得到满足,并超时。

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

  1. 使testingMethod()返回一个Future (正如您在评论中所想的那样):这不允许断言异步执行,但它避免了太多等待;
  2. runAsync()方法包装在您可以模拟的另一个服务中,并捕获参数;
  3. 如果您使用的是 Spring ,请将 lambda 表达式移动到另一个服务,方法是使用@Async注释。 这允许轻松地模拟和单元测试该服务,并消除直接调用runAsync()的负担;
  4. 使用自定义 executor ,并将其传递给runAsync()

如果您使用的是 Spring,我建议您使用第三种解决方案,因为它确实是最干净的,并且可以避免到处都使用runAsync()调用使您的代码混乱。

选项 2 和 4 非常相似,它只是改变了你必须模拟的内容。

如果您使用 go 进行第四种解决方案,您可以这样做:

更改测试 class 以使用自定义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();
    }
}

你可以有一个TimeUnit.SECONDS.sleep(1); testObject.testingMethod(); 在你的测试中。

附带说明一下,我什至不认为您应该测试异步发生的事情是否已完成或已调用或其他,这不是此 function 的责任。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM