使用Mockito监视CompletableFuture时,spyObj.get有时会失败

时间:2018-06-29 21:31:43

标签: java mockito completable-future

我遇到了一个问题,在运行测试套件时,下面的示例代码有时会失败,但是个别测试似乎总是通过。如果我仅对间谍CompletableFuture使用.get()而不指定超时,它将无限期挂起。

在Windows,OS X上都会出现此问题,并且我尝试了Java 8 JDK的一些不同版本。

我在Mockito 2.18.3和Mockito 1.10.19中遇到这个问题。

我有时可以在下面成功地运行示例测试套件代码7至10次,但是尝试超过10次时,几乎总是可以看到随机测试失败。

任何帮助将不胜感激。我也发布在了Mockito邮件列表中,但那里的情况看起来相当。

package example;


import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import org.junit.Test;
import static org.mockito.Mockito.spy;


public class MockitoCompletableFuture1Test {

    @Test
    public void test1() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

    @Test
    public void test2() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

    @Test
    public void test3() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

    @Test
    public void test4() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

    @Test
    public void test5() throws Exception {

        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "ABC");
        CompletableFuture<String> futureSpy = spy(future);

        try {
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            assertEquals("ABC", future.get(1, TimeUnit.SECONDS));    // PASSES
            assertEquals("ABC", futureSpy.get(1, TimeUnit.SECONDS)); // OCCASIONALLY FAILS
            fail("futureSpy.get(...) timed out");
        }

    }

}

2 个答案:

答案 0 :(得分:2)

创建future(调用CompletableFuture.supplyAsync)时,它还将创建一个线程(ForkJoinPool.commonPool-worker-N)来执行lambda表达式。该线程引用了新创建的对象(在本例中为future)。异步作业完成后,线程(ForkJoinPool.commonPool-worker-N)将通知(唤醒)另一个线程(main)等待它完成。

它如何知道哪个线程正在等待它?当您调用get()方法时,当前线程将被保存为类中的字段,并且该线程将驻留(休眠)并等待被其他线程取消驻留。

问题是futureSpy将当前线程(main)保存在其自己的字段中,但是异步线程将尝试从future对象({{ 1}})。

问题不会总是出现在您的测试用例中,因为如果异步功能已经完成,null不会使主线程进入睡眠状态。


简化示例

出于测试目的,我将您的测试用例减少到可以可靠地重现错误的时间(首次运行除外):

get

在我的测试中,输出为:

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import static org.mockito.Mockito.spy;

public class App {
   public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
      for (int i = 0; i < 100; i++) {
         CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
               Thread.sleep(500);
            } catch (InterruptedException e) {
               throw new RuntimeException(e);
            }
            return "ABC";
         });
         CompletableFuture<String> futureSpy = spy(future);
         try {
            futureSpy.get(2, TimeUnit.SECONDS);
            System.out.println("i = " + i);
         } catch (TimeoutException ex) {
            System.out.println("i = " + i + " FAIL");
         }
      }
   }
}

答案 1 :(得分:0)

根据Important gotcha on spying real objects!

  

Mockito *不会* 将调用委托给传递的真实实例,而是实际上创建它的副本。因此,如果保留真实实例并与之交互,请不要期望间谍知道这些交互及其对真实实例状态的影响。 […]

因此,基本上,它将取决于您在致电spy() 时的状态。如果已经完成,那么间谍也将成为间谍。否则,您的间谍将保持未完成状态,除非您自己完成。

由于异步完成将在最初的将来而不是在您的间谍上执行,因此它不会反映在您的间谍上。

唯一可行的情况是您完全控制了它。这意味着您将用CompletableFuture创建new,将其包装为间谍,然后仅使用该间谍。

但是,一般来说,我建议避免模拟期货,因为您通常无法控制期货的处理方式。如Mockito's Remember section中所述:

  

请勿嘲笑您不拥有的类型

CompletableFuture不是您拥有的类型。

无论如何,不​​必模拟CompletableFuture方法,因为您可以基于complete()completeExecptionally()控制它们的作用。另一方面,由于以下原因,不必检查其方法是否被调用:

  • 具有副作用的方法(例如complete())可以在之后轻松声明;
  • 其他方法返回的值将使您的测试在不使用时失败。

CompletableFuture的行为基本上类似于值对象,并且文档指出:

  

不要嘲笑值对象

如果您觉得不使用间谍就无法编写测试,请尝试将其简化为MCVE并发布有关如何执行测试的单独问题。