【问题标题】:How can I test a method call inside an asynchronous operation in unit testing如何在单元测试中测试异步操作中的方法调用
【发布时间】:2020-02-19 13:00:44
【问题描述】:

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

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

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

【问题讨论】:

    标签: java asynchronous mockito junit5 completable-future


    【解决方案1】:

    如果您在本主题中寻求更多建议,请尝试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

    【讨论】:

      【解决方案2】:

      问题归结为测试方法调用静态方法:CompletableFuture.runAsync()。静态方法通常对模拟和断言几乎没有控制。

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

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

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

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

      (你也可以@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();
          }
      }
      

      【讨论】:

      • 感谢您的回复。我已经通过返回 Future(您的第一个建议)解决了我的问题。这解决了我的问题。我对你的第三个建议很感兴趣,我稍后会尝试。
      【解决方案3】:

      您可以在测试中在testObject.testingMethod(); 之后添加TimeUnit.SECONDS.sleep(1);

      顺便说一句,我什至认为您不应该测试异步发生的事情是否已完成或调用或其他任何事情,这不是该函数的责任。

      【讨论】:

      • 这是一种好的睡眠方式吗?因为它只是在等待一定的时间,但异步操作可能还没有执行。我还想解释一下为什么方法有这样的结构:首先进行基本操作,之后可以继续执行主要代码,但我还需要执行一些相当长的任务,这些任务是必需的,但不是必需的主要代码。因此,这样做是为了加快主代码的速度。尽管如此,这是适用于它的方法的重要部分,我也想测试执行这些操作的事实。
      • 使用您当前的代码,无法确定。因为你正在做所谓的“一劳永逸”。只要执行了异步块,您就不会关心何时执行异步块。如果你关心它,你可以在你的方法中等待它,做一些事情,比如在异步部分验证一切顺利。这将使您的方法在异步代码完成后完成,从而使其完全可测试。
      • 好的,我明白了,谢谢你的帮助。也许我应该从这个方法返回未来,这将允许我在测试中调用 get() 并检查方法调用。
      猜你喜欢
      • 2012-05-20
      • 1970-01-01
      • 2013-03-14
      • 2017-10-14
      • 2014-02-27
      • 2017-12-21
      • 2020-07-14
      相关资源
      最近更新 更多