我遇到了一个问题,在运行测试套件时,下面的示例代码有时会失败,但是个别测试似乎总是通过。如果我仅对间谍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");
}
}
}
答案 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并发布有关如何执行测试的单独问题。