bounty将在6天后过期。回答此问题可获得+250的声誉奖励。PacificNW_Lover正在寻找来自声誉良好的来源的答案。
拥有一个非特定于服务器的通用框架,用于导入和重用微服务中的对象,但可用于任何支持Java8的特定Java程序。
任务是创建一个retry()
机制,该机制将采用以下两个参数:
Supplier<CompletableFuture<R>> supplier
int numRetries
我需要它做的是创建一个泛型组件,每当基于指定的numRetries
出现异常时,该组件都会对CompletableFuture
执行重试操作。
使用Java8,我编写了下面的泛型helper类,它使用Supplier根据指定的重试次数重试方法。
一般来说,我是CompletableFuture
的新手,所以我想知道如何为这个方法编写一个JUnit 5(以及Mockito是否更好)测试?我测试了所有的边缘情况,并尽可能地使它通用,以供其他人重用。
请注意,这是在内部公共框架中,由其他微服务作为Maven依赖项导入,但目的是能够在JavaSE级别重用它。
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
的配置文件中放入一个错误的URL,并且log.info(Error getting orderResponse = {});
通过grep
对app.log
文件打印了两次。
这是将上述类作为Maven依赖项导入的调用类:
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());
}
}
}
虽然这个例子的调用类使用OrderSerice
,并且HttpClient
进行调用,但是这是一个泛型实用类,专门编写为可重用于返回CompletableFuture<OrderReponse>
的任何类型的方法调用。
问题:
1.如何为此编写JUnit 5测试用例(或Mockito):public static <R> CompletableFuture<R> RetryUtility.retry(Supplier<CompletableFuture<R>> supplier, int numRetries)
方法?
1.在设计和/或实现中,是否存在任何边缘情况或细微差别?
1条答案
按热度按时间g52tjvyc1#
在你问“我如何测试方法X?”之前,澄清“X实际上是做什么的?"会有帮助。为了回答后一个问题,我个人喜欢以一种方式重写方法,这样各个步骤就变得更清楚了。
对
retry
方法执行此操作,可以得到:注意,我删除了不必要的
if
语句和log.info
调用,后者是不可测试的,除非你把一个logger示例作为方法参数传入(或者把retry
设置为非静态的,把logger作为一个示例变量通过构造函数传入,或者使用一些肮脏的技巧,比如重定向logger输出流)。那么,
retry
实际上是做什么的呢?1.它调用
supplier.get()
一次,并将值赋给completableFuture
变量。1.如果为
numRetries <= 0
,则返回completableFuture
,该示例与Supplier的get
方法返回的CF示例相同。1.如果
numRetries > 0
,则执行以下步骤numRetries
次:1.它创建一个函数
function1
,返回一个完整的CF。1.它将
function1
传递给completableFuture
的thenApply
方法,从而创建一个新的CFtmp1
。1.它创建了另一个函数
function2
,该函数忽略其输入参数,但调用supplier.get()
。1.它将
function2
传递给tmp1
的exceptionally
方法,从而创建一个新的CFtmp2
。1.它创建了第三个函数
function3
,它是恒等函数。1.它将
function3
传递给tmp2
的thenCompose
,创建一个新的CF并将其赋给completableFuture
变量。对一个函数做什么有一个清晰的分解,让你看到你可以测试什么,你不能测试什么,因此,你可能想要重构。步骤1和2是非常容易测试的:
在步骤1中,模拟一个
Supplier
并测试其get
方法是否被调用:在步骤2中,让供应商返回一些预定义的示例,并检查
retry
是否返回相同的示例:当然,棘手的部分是第3步,首先要注意的是哪些变量会受到输入参数的影响,这些变量是:
completableFuture
、tmp1
、function2
和tmp2
。相比之下,function1
和function3
实际上是常量,不受输入参数的影响。那么,我们如何影响列出的参数呢?
completableFuture
是Supplier
的get
方法的返回值。如果我们让get
返回一个mock,我们可以影响thenApply
的返回值。类似地,如果我们让thenApply
返回一个mock,我们可以影响expectionally
的返回值,也可以对thenCompose
应用相同的逻辑,然后测试我们的方法是否以正确的顺序被调用了正确的次数:那么
thenApply
和thenCompose
的方法参数呢?没有办法测试这些方法是否分别用function1
和function3
调用过,(除非你重构代码,把函数移出方法调用),因为这些函数是我们方法的局部函数,不受参数的影响。虽然我们无法测试exceptionally
是否被function2
作为参数调用,但我们可以测试function2
调用supplier.get()
的次数是否正好为numRetries
。类似地,您可以通过提供一个供应商来测试
retry
调用get
方法n+1
次,该供应商在调用n
次后返回一个完整的future。我们仍然需要做的(也是我们可能应该做的第一步)是测试我们的方法的返回值是否正确:
numRetries
次失败,则CF应异常完成。numRetries
次,则CF应正常完成。您可能需要考虑测试和捕获的其他一些情况是,如果
numRetries
为负或非常大,会发生什么?这样的方法调用是否会抛出异常?另一个我们还没有触及的问题是:这是测试代码的正确方法吗?
有些人可能会说是的,有些人可能会说,你不应该测试你的方法的内部结构,而应该只测试它的输出(也就是说,基本上只测试最后两次)。这显然是有争议的,而且像大多数事情一样,取决于你的需求。(例如,你会测试一些排序算法的内部结构吗?比如数组赋值等等?)
正如您所看到的,通过测试内部结构,测试变得相当复杂,并且涉及到大量的模拟。测试内部结构也会使重构变得更加麻烦,因为您的测试可能会开始失败,即使您没有更改方法的逻辑。就个人而言,在大多数情况下我不会编写这样的测试。然而,如果很多人依赖于我的代码的正确性,例如执行步骤的顺序,我可能会考虑它。
无论如何,如果您选择走那条路,我希望这些例子对您有用。