[英]How to write JUnit5 or Mockito test for a retry mechanism built using Supplier<CompletableFuture> as a method param?
有一個不是特定於服務器的通用框架,我們用它來導入和重用我們微服務中的對象,但可以用於支持 Java 的任何特定 Java 程序 8.
負責創建一個retry()
機制,該機制將采用以下兩個參數:
Supplier<CompletableFuture<R>> supplier
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 使用OrderSerice
和HttpClient
進行調用,但這是一個通用實用程序 class,它專門編寫為可重復用於返回CompletableFuture<OrderReponse>
的任何類型的方法調用。
問題):
如何為此編寫一個 JUnit 5 測試用例(或 Mockito): public static <R> CompletableFuture<R> RetryUtility.retry(Supplier<CompletableFuture<R>> supplier, int numRetries)
方法?
有人可以在其設計和/或實施中看到任何邊緣情況或細微差別嗎?
在您問“我如何測試方法 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
實際上做了什么?
supplier.get()
一次並將值分配給completableFuture
變量。numRetries <= 0
,則它返回completableFuture
,它與供應商的get
方法返回的 CF 實例相同。numRetries > 0
,則它執行以下步驟numRetries
次:
function1
1。function1
傳遞給completableFuture
的thenApply
方法,創建一個新的 CF tmp1
。function2
2,它是一個 function 忽略其輸入參數但調用supplier.get()
。function2
傳遞給tmp1
的exceptionally
方法,創建一個新的 CF tmp2
。function3
3,即標識 function。function3
傳遞給tmp2
的thenCompose
,創建一個新的 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 影響。它們是: completableFuture
、 tmp1
、 function2
和tmp2
。 相反, function1
和function3
實際上是常量,不受我們輸入 arguments 的影響。
那么,我們如何影響上市的arguments呢? 好吧, completableFuture
是我們Supplier
的get
方法的返回值。 如果我們讓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();
}
那么thenApply
和thenCompose
的方法 arguments 呢? 無法測試這些方法是否已分別用function1
和function3
調用(除非您重構代碼並將函數移出該方法調用),因為這些函數是我們方法的本地函數,不受我們的 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
次后返回一個完整的未來。
我們仍然需要做的(以及我們可能應該做的第一步)是測試我們方法的返回值是否正確運行:
numRetries
失敗,則 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.