[英]When spying on a CompletableFuture with Mockito, spyObj.get occasionally fails
我遇到了一個問題,在運行測試套件時,下面的示例代碼有時會失敗,但是個別測試似乎總是通過。 如果我僅將.get()用於間諜CompletableFuture而未指定超時,則它將無限期掛起。
在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,然后發布有關如何執行測試的單獨問題。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.