我們繼續上一節,繼續使用 spock 測試我們自己封裝的 WebClient
測試針對 readTimeout 重試
針對響應超時,我們需要驗證重試僅針對可以重試的方法(包括 GET 方法以及配置的可重試方法),針對不可重試的方法沒有重試。我們可以通過 spock 單元測試中,檢查對於負載均衡器獲取例項方法的呼叫次數看出來是否有重試
我們通過 httpbin.org 的 '/delay/秒' 實現 readTimeout,分別驗證:
- 測試 GET 延遲 2 秒返回,超過讀取超時,這時候會重試
- 測試 POST 延遲 3 秒返回,超過讀取超時,同時路徑在重試路徑中,這樣也是會重試的
- 測試 POST 延遲 2 秒返回,超過讀取超時,同時路徑在重試路徑中,這樣不會重試
程式碼如下:
@SpringBootTest(
properties = [
"webclient.configs.testServiceWithCannotConnect.baseUrl=http://testServiceWithCannotConnect",
"webclient.configs.testServiceWithCannotConnect.serviceName=testServiceWithCannotConnect",
"webclient.configs.testService.baseUrl=http://testService",
"webclient.configs.testService.serviceName=testService",
"webclient.configs.testService.responseTimeout=1s",
"webclient.configs.testService.retryablePaths[0]=/delay/3",
"webclient.configs.testService.retryablePaths[1]=/status/4*",
"spring.cloud.loadbalancer.zone=zone1",
"resilience4j.retry.configs.default.maxAttempts=3",
"resilience4j.circuitbreaker.configs.default.failureRateThreshold=50",
"resilience4j.circuitbreaker.configs.default.slidingWindowType=TIME_BASED",
"resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
//因為重試是 3 次,為了防止斷路器開啟影響測試,設定為正好比重試多一次的次數,防止觸發
//同時我們在測試的時候也需要手動清空斷路器統計
"resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=4",
"resilience4j.circuitbreaker.configs.default.recordExceptions=java.lang.Exception"
],
classes = MockConfig
)
class WebClientUnitTest extends Specification {
@SpringBootApplication
static class MockConfig {
}
@SpringBean
private LoadBalancerClientFactory loadBalancerClientFactory = Mock()
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry
@Autowired
private Tracer tracer
@Autowired
private ServiceInstanceMetrics serviceInstanceMetrics
@Autowired
private WebClientNamedContextFactory webClientNamedContextFactory
//不同的測試方法的類物件不是同一個物件,會重新生成,保證互相沒有影響
def zone1Instance1 = new DefaultServiceInstance(instanceId: "instance1", host: "www.httpbin.org", port: 80, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
def zone1Instance2 = new DefaultServiceInstance(instanceId: "instance2", host: "www.httpbin.org", port: 8081, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
def zone1Instance3 = new DefaultServiceInstance(instanceId: "instance3", host: "httpbin.org", port: 80, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance = Spy();
ServiceInstanceListSupplier serviceInstanceListSupplier = Spy();
//所有測試的方法執行前會呼叫的方法
def setup() {
//初始化 loadBalancerClientFactoryInstance 負載均衡器
loadBalancerClientFactoryInstance.setTracer(tracer)
loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
}
def "測試針對 readTimeout 重試"() {
given: "設定 testService 的例項都是正常例項"
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))
when: "測試 GET 延遲 2 秒返回,超過讀取超時"
//清除斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.get().uri("/delay/2").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientRequestException e) {
if (e.getCause() in ReadTimeoutException) {
//讀取超時忽略
} else {
throw e;
}
}
then: "每次都會超時所以會重試,根據配置一共有 3 次"
3 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "測試 POST 延遲 3 秒返回,超過讀取超時,同時路徑在重試路徑中"
//清除斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.post().uri("/delay/3").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientRequestException e) {
if (e.getCause() in ReadTimeoutException) {
//讀取超時忽略
} else {
throw e;
}
}
then: "每次都會超時所以會重試,根據配置一共有 3 次"
3 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "測試 POST 延遲 2 秒返回,超過讀取超時,這個不能重試"
//清除斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.post().uri("/delay/2").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientRequestException e) {
if (e.getCause() in ReadTimeoutException) {
//讀取超時忽略
} else {
throw e;
}
}
then: "沒有重試,只有一次呼叫"
1 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}
測試非 2xx 響應碼返回的重試
對於非 2xx 的響應碼,代表請求失敗,我們需要測試:
- 測試 GET 返回 500,會有重試
- 測試 POST 返回 500,沒有重試
- 測試 POST 返回 400,這個請求路徑在重試路徑中,會有重試
@SpringBootTest(
properties = [
"webclient.configs.testServiceWithCannotConnect.baseUrl=http://testServiceWithCannotConnect",
"webclient.configs.testServiceWithCannotConnect.serviceName=testServiceWithCannotConnect",
"webclient.configs.testService.baseUrl=http://testService",
"webclient.configs.testService.serviceName=testService",
"webclient.configs.testService.responseTimeout=1s",
"webclient.configs.testService.retryablePaths[0]=/delay/3",
"webclient.configs.testService.retryablePaths[1]=/status/4*",
"spring.cloud.loadbalancer.zone=zone1",
"resilience4j.retry.configs.default.maxAttempts=3",
"resilience4j.circuitbreaker.configs.default.failureRateThreshold=50",
"resilience4j.circuitbreaker.configs.default.slidingWindowType=TIME_BASED",
"resilience4j.circuitbreaker.configs.default.slidingWindowSize=5",
//因為重試是 3 次,為了防止斷路器開啟影響測試,設定為正好比重試多一次的次數,防止觸發
//同時我們在測試的時候也需要手動清空斷路器統計
"resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=4",
"resilience4j.circuitbreaker.configs.default.recordExceptions=java.lang.Exception"
],
classes = MockConfig
)
class WebClientUnitTest extends Specification {
@SpringBootApplication
static class MockConfig {
}
@SpringBean
private LoadBalancerClientFactory loadBalancerClientFactory = Mock()
@Autowired
private CircuitBreakerRegistry circuitBreakerRegistry
@Autowired
private Tracer tracer
@Autowired
private ServiceInstanceMetrics serviceInstanceMetrics
@Autowired
private WebClientNamedContextFactory webClientNamedContextFactory
//不同的測試方法的類物件不是同一個物件,會重新生成,保證互相沒有影響
def zone1Instance1 = new DefaultServiceInstance(instanceId: "instance1", host: "www.httpbin.org", port: 80, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
def zone1Instance2 = new DefaultServiceInstance(instanceId: "instance2", host: "www.httpbin.org", port: 8081, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
def zone1Instance3 = new DefaultServiceInstance(instanceId: "instance3", host: "httpbin.org", port: 80, metadata: Map.ofEntries(Map.entry("zone", "zone1")))
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance = Spy();
ServiceInstanceListSupplier serviceInstanceListSupplier = Spy();
//所有測試的方法執行前會呼叫的方法
def setup() {
//初始化 loadBalancerClientFactoryInstance 負載均衡器
loadBalancerClientFactoryInstance.setTracer(tracer)
loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
}
def "測試非 200 響應碼返回" () {
given: "設定 testService 的例項都是正常例項"
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))
when: "測試 GET 返回 500"
//清除斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.get().uri("/status/500").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientResponseException e) {
if (e.getStatusCode().is5xxServerError()) {
//5xx忽略
} else {
throw e;
}
}
then: "每次都沒有返回 2xx 所以會重試,根據配置一共有 3 次"
3 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "測試 POST 返回 500"
//清除斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.post().uri("/status/500").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientResponseException e) {
if (e.getStatusCode().is5xxServerError()) {
//5xx忽略
} else {
throw e;
}
}
then: "POST 預設不重試,所以只會呼叫一次"
1 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "測試 POST 返回 400,這個請求路徑在重試路徑中"
//清除斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
try {
webClientNamedContextFactory.getWebClient("testService")
.post().uri("/status/400").retrieve()
.bodyToMono(String.class).block();
} catch (WebClientResponseException e) {
if (e.getStatusCode().is4xxClientError()) {
//4xx忽略
} else {
throw e;
}
}
then: "路徑在重試路徑中,所以會重試"
3 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}
微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer: