簡體   English   中英

使用Mockito監視CompletableFuture時,spyObj.get有時會失敗

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM