当使用Mockito监视CompletableFuture时,spyObj.get偶尔会失败



我遇到了一个问题,在运行测试套件时,我下面的示例代码偶尔会失败,但单独的测试似乎总是通过。如果我只对间谍 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");
}
}
}

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

它如何知道哪个线程正在等待它?调用get()方法时,当前线程将另存为类中的字段,线程将停放(睡眠(并等待其他线程取消停放。

问题是futureSpy会将当前线程(main(保存在自己的字段中,但异步线程将尝试从future对象(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");
}
}
}
}

在我的测试中,输出为:

i = 0
i = 1 FAIL
i = 2 FAIL
i = 3 FAIL

根据间谍真实物体的重要陷阱!

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

所以基本上,它会在你打电话给spy()的时候,你的未来状态。如果它已经完成,那么由此产生的间谍也将完成。否则,您的间谍将保持未完成状态,除非您自己完成。

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

唯一可以正常工作的情况是,您可以完全控制它。这意味着您将使用new创建您的CompletableFuture,将其包装在间谍中,然后仅使用该间谍。

然而,总的来说,我建议避免嘲笑期货,因为你通常无法控制它们的处理方式。正如Mockito的"记住"部分所述:

不要嘲笑你不拥有的类型

CompletableFuture不是您拥有的类型。

无论如何,没有必要模拟CompletableFuture方法,因为您可以根据complete()completeExecptionally()来控制它们的作用。另一方面,没有必要检查是否调用了它的方法,因为:

  • 有副作用的方法(如complete()(可以很容易地在事后断言;
  • 其他方法是返回值,如果不使用这些值,这些值应使测试失败。

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

不要模拟值对象

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

最新更新