簡體   English   中英

如何為使用 Supplier 構建的重試機制編寫 JUnit5 或 Mockito 測試<completablefuture>作為方法參數? </completablefuture>

[英]How to write JUnit5 or Mockito test for a retry mechanism built using Supplier<CompletableFuture> as a method param?

有一個不是特定於服務器的通用框架,我們用它來導入和重用我們微服務中的對象,但可以用於支持 Java 的任何特定 Java 程序 8.

負責創建一個retry()機制,該機制將采用以下兩個參數:

  1. Supplier<CompletableFuture<R>> supplier
  2. int numRetries

我需要它做的是制作一個通用組件,每次根據指定的numRetries出現異常時,該組件都會對CompletableFuture進行重試操作。

使用 Java 8,我編寫了以下通用幫助程序 class,它使用供應商根據指定的重試次數重試方法。

一般來說,我是CompletableFuture的新手,所以我想知道如何為此方法編寫 JUnit 5(如果 Mockito 更好)測試? 我正在測試所有邊緣情況,並已使其盡可能通用,以供其他人重復使用。

請注意,這是在一個內部通用框架內,該框架由其他微服務作為 Maven 依賴項導入,但目的是能夠在 Java SE 級別重用它。

public class RetryUtility {

    private static final Logger log = 
                    LoggerFactory.getLogger(RetryUtility.class);

    public static <R> CompletableFuture<R> retry(
                                       Supplier<CompletableFuture<R>> supplier, 
                                       int numRetries) {
        CompletableFuture<R> completableFuture = supplier.get();
        if (numRetries > 0) {
            for (int i = 0; i < numRetries; i++) {
                completableFuture = 
                            completableFuture
                              .thenApply(CompletableFuture::completedFuture)
                              .exceptionally(
                                  t -> {
                                   log.info("Retrying for {}", t.getMessage());
                                   return supplier.get();
                                })
                              .thenCompose(Function.identity());
            }
        }
        return completableFuture;
    }
}

用法:假設這段代碼可以編譯和工作,我能夠在client和 log.info 的配置文件中輸入錯誤的log.info(Error getting orderResponse = {}); 通過grep針對app.log文件打印了兩次。

這是將上述 class 導入為 Maven 依賴項的調用 class:

public class OrderServiceFuture {

    private CompletableFuture<OrderReponse> getOrderResponse(
                           OrderInput orderInput,BearerToken token) {
        int numRetries = 1;
        CompletableFuture<OrderReponse> orderResponse =
               RetryUtility.retry(() -> makeOrder(orderInput, token), numRetries);
        orderResponse.join();
        return orderResponse;
    }

    public CompletableFuture<OrderReponse> makeOrder() {
        return client.post(orderInput, token),
            orderReponse -> {
            log.info("Got orderReponse = {}", orderReponse);
        },
        throwable -> {
            log.error("Error getting orderResponse = {}", throwable.getMessage());
        }
    }
}

盡管此示例的調用 class 使用OrderSericeHttpClient進行調用,但這是一個通用實用程序 class,它專門編寫為可重復用於返回CompletableFuture<OrderReponse>的任何類型的方法調用。

問題):

  1. 如何為此編寫一個 JUnit 5 測試用例(或 Mockito): public static <R> CompletableFuture<R> RetryUtility.retry(Supplier<CompletableFuture<R>> supplier, int numRetries)方法?

  2. 有人可以在其設計和/或實施中看到任何邊緣情況或細微差別嗎?

在您問“我如何測試方法 X?”之前它有助於澄清“X 實際上做了什么?”。 為了回答后一個問題,我個人喜歡以一種使各個步驟變得更加清晰的方式重寫方法。

為您的retry方法這樣做,我得到:

public static <R> CompletableFuture<R> retry(
        Supplier<CompletableFuture<R>> supplier,
        int numRetries
) {
    CompletableFuture<R> completableFuture = supplier.get();
    for (int i = 0; i < numRetries; i++) {
        Function<R, CompletableFuture<R>> function1 = CompletableFuture::completedFuture;
        CompletableFuture<CompletableFuture<R>> tmp1 = completableFuture.thenApply(function1);
        
        Function<Throwable, CompletableFuture<R>> function2 = t -> supplier.get();
        CompletableFuture<CompletableFuture<R>> tmp2 = tmp1.exceptionally(function2);
        
        Function<CompletableFuture<R>, CompletableFuture<R>> function3 = Function.identity();
        completableFuture = tmp2.thenCompose(function3);
    }
    return completableFuture;
}

請注意,我刪除了不必要的if語句以及log.info調用。 后者不可測試,除非您將記錄器實例作為方法參數傳遞(或者使retry成為非靜態的,並將記錄器作為通過構造函數傳遞的實例變量,或者可能使用一些骯臟的技巧,例如重定向記錄器 output溪流)。

現在, retry實際上做了什么?

  1. 它調用supplier.get()一次並將值分配給completableFuture變量。
  2. 如果numRetries <= 0 ,則它返回completableFuture ,它與供應商的get方法返回的 CF 實例相同。
  3. 如果numRetries > 0 ,則它執行以下步驟numRetries次:
    1. 它創建一個返回完整 CF 的 function function1 1。
    2. 它將function1傳遞給completableFuturethenApply方法,創建一個新的 CF tmp1
    3. 它創建另一個 function function2 2,它是一個 function 忽略其輸入參數但調用supplier.get()
    4. 它將function2傳遞給tmp1exceptionally方法,創建一個新的 CF tmp2
    5. 它創建了第三個 function function3 3,即標識 function。
    6. 它將function3傳遞給tmp2thenCompose ,創建一個新的 CF 並將其分配給completableFuture變量。

清楚地分解 function 的作用可以讓您了解可以測試的內容和無法測試的內容,因此可能需要重構。 步驟 1. 和 2. 非常容易測試:

對於第 1 步,您模擬一個Supplier並測試它的get方法是否被調用:

@Test
void testStep1() { // please use more descriptive names...
    Supplier<CompletableFuture<Object>> mock = Mockito.mock(Supplier.class);
    RetryUtility.retry(mock, 0);
    Mockito.verify(mock).get();
    Mockito.verifyNoMoreInteractions(mock);
}

對於第 2 步,您讓供應商返回一些預定義的實例並檢查retry是否返回相同的實例:

@Test
void testStep2() {
    CompletableFuture<Object> instance = CompletableFuture.completedFuture("");
    Supplier<CompletableFuture<Object>> supplier = () -> instance;
    CompletableFuture<Object> result = RetryUtility.retry(supplier, 0);
    Assertions.assertSame(instance, result);
}

棘手的部分當然是第 3 步。您應該注意的第一件事是哪些變量受我們的輸入 arguments 影響。它們是: completableFuturetmp1function2tmp2 相反, function1function3實際上是常量,不受我們輸入 arguments 的影響。

那么,我們如何影響上市的arguments呢? 好吧, completableFuture是我們Supplierget方法的返回值。 如果我們讓get返回一個模擬,我們可以影響thenApply的返回值。 同樣,如果我們讓thenApply返回一個 mock,我們可以影響expectionally的返回值。 我們可以將相同的邏輯應用於thenCompose ,然后測試我們的方法是否以正確的順序被調用了正確的次數:

@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 3, 5, 10, 100})
void testStep3(int numRetries) {
    CompletableFuture<Object> completableFuture = mock(CompletableFuture.class);
    CompletableFuture<CompletableFuture<Object>> tmp1 = mock(CompletableFuture.class);
    CompletableFuture<CompletableFuture<Object>> tmp2 = mock(CompletableFuture.class);

    doReturn(tmp1).when(completableFuture).thenApply(any(Function.class));
    doReturn(tmp2).when(tmp1).exceptionally(any(Function.class));
    doReturn(completableFuture).when(tmp2).thenCompose(any(Function.class));

    CompletableFuture<Object> retry = RetryUtility.retry(
            () -> completableFuture, // here 'get' returns our mock
            numRetries
    );
    // While we're at it we also test that we get back our initial CF
    Assertions.assertSame(completableFuture, retry);

    InOrder inOrder = Mockito.inOrder(completableFuture, tmp1, tmp2);
    for (int i = 0; i < numRetries; i++) {
        inOrder.verify(completableFuture, times(1)).thenApply(any(Function.class));
        inOrder.verify(tmp1, times(1)).exceptionally(any(Function.class));
        inOrder.verify(tmp2, times(1)).thenCompose(any(Function.class));
    }
    inOrder.verifyNoMoreInteractions();
}

那么thenApplythenCompose的方法 arguments 呢? 無法測試這些方法是否已分別用function1function3調用(除非您重構代碼並將函數移出該方法調用),因為這些函數是我們方法的本地函數,不受我們的 arguments 的影響。但是function2呢? 雖然我們無法測試以function2作為參數exceptionally調用,但我們可以測試function2調用supplier.get()恰好numRetries次。 它什么時候這樣做? 好吧,只有當 CF 失敗時:

@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 3, 5, 10, 100})
void testStep3_4(int numRetries) {
    CompletableFuture<Object> future = CompletableFuture.failedFuture(new RuntimeException());
    Supplier<CompletableFuture<Object>> supplier = mock(Supplier.class);
    doReturn(future).when(supplier).get();

    Assertions.assertThrows(
            CompletionException.class,
            () -> RetryUtility.retry(supplier, numRetries).join()
    );
    
    // remember: supplier.get() is also called once at the beginning 
    Mockito.verify(supplier, times(numRetries + 1)).get();
    Mockito.verifyNoMoreInteractions(supplier);
}

同樣,您可以通過提供供應商來測試retry調用get方法n+1次,該供應商在調用n次后返回一個完整的未來。

我們仍然需要做的(以及我們可能應該做的第一步)是測試我們方法的返回值是否正確運行:

  • 如果 CF 嘗試numRetries失敗,則 CF 應該異常完成。
  • 如果 CF 失敗次數少於numRetries嘗試次數,則 CF 應該正常完成。
@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 3, 5, 10})
void testFailingCf(int numRetries) {
    RuntimeException exception = new RuntimeException("");
    CompletableFuture<Object> future = CompletableFuture.failedFuture(exception);

    CompletionException completionException = Assertions.assertThrows(
            CompletionException.class,
            () -> RetryUtility.retry(() -> future, numRetries).join()
    );
    Assertions.assertSame(exception, completionException.getCause());
}

@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 3, 5, 10})
void testSucceedingCf(int numRetries) {
    final AtomicInteger counter = new AtomicInteger();
    final String expected = "expected";

    Supplier<CompletableFuture<Object>> supplier =
            () -> (counter.getAndIncrement() < numRetries / 2)
                    ? CompletableFuture.failedFuture(new RuntimeException())
                    : CompletableFuture.completedFuture(expected);

    Object result = RetryUtility.retry(supplier, numRetries).join();

    Assertions.assertEquals(expected, result);
    Assertions.assertEquals(numRetries / 2 + 1, counter.get());
}

您可能要考慮測試和捕獲的其他一些情況是,如果numRetries為負數或非常大,會發生什么情況? 這樣的方法調用應該拋出異常嗎?


到目前為止我們還沒有觸及的另一個問題是:這是測試代碼的正確方法嗎?

有些人可能會說是,其他人會爭辯說你不應該測試你的方法的內部結構,而應該只測試它的 output 給定一些輸入(即基本上只有最后兩個測試)。 這顯然值得商bat,並且像大多數事情一樣,取決於您的要求。 (例如,你會測試一些排序算法的內部結構,比如數組賦值等等嗎?)。

如您所見,通過測試內部結構,測試變得相當復雜,涉及很多 mocking。測試內部結構也會使重構更加麻煩,因為您的測試可能會開始失敗,即使您沒有更改你的方法的邏輯。 就個人而言,在大多數情況下我不會編寫這樣的測試。 但是,如果很多人都依賴於我的代碼的正確性,例如步驟執行的順序,我可能會考慮。

不管怎樣,如果你選擇go那條路由,希望這些例子對你有用。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

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