我們來測試下前面封裝好的 WebClient,這裡開始,我們使用 spock 編寫 groovy 單元測試,這種編寫出來的單元測試,程式碼更加簡潔,同時更加靈活,我們在接下來的單元測試程式碼中就能看出來。
編寫基於 spock 的 spring-boot context 測試
我們加入前面設計的配置,編寫測試類:
@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 {
}
}
我們加入三個服務例項供單元測試呼叫:
class WebClientUnitTest extends Specification {
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")))
}
我們要動態的指定負載均衡獲取服務例項列表的響應,即去 Mock 負載均衡器的 ServiceInstanceListSupplier 並覆蓋:
class WebClientUnitTest extends Specification {
@Autowired
private Tracer tracer
@Autowired
private ServiceInstanceMetrics serviceInstanceMetrics
RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance = Spy();
ServiceInstanceListSupplier serviceInstanceListSupplier = Spy();
//所有測試的方法執行前會呼叫的方法
def setup() {
//初始化 loadBalancerClientFactoryInstance 負載均衡器
loadBalancerClientFactoryInstance.setTracer(tracer)
loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
}
}
之後,我們可以通過下面的 groovy 程式碼,動態指定微服務返回例項:
//指定 testService 微服務的 LoadBalancer 為 loadBalancerClientFactoryInstance
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
//指定 testService 微服務例項列表為 zone1Instance1, zone1Instance3
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))
測試斷路器異常重試以及斷路器級別
我們需要驗證:
- 對於斷路器開啟的異常,由於沒有請求發出去,所以需要直接重試其他的例項。我們可以設立一個微服務,包含兩個例項,將其中一個例項的某個路徑斷路器開啟,之後多次呼叫這個微服務的這個路徑介面,看是否都呼叫成功(由於有重試,所以每次呼叫都會成功)。同時驗證,對於負載均衡器獲取服務例項的呼叫,多於呼叫次數(每次重試都會呼叫負載均衡器獲取一個新的例項用於呼叫)
- 某個路徑斷路器開啟的時候,其他路徑斷路器不會開啟。在上面開啟一個微服務某個例項的一個路徑的斷路器之後,我們呼叫其他的路徑,無論多少次,都成功並且呼叫負載均衡器獲取服務例項的次數等於呼叫次數,代表沒有重試,也就是沒有斷路器異常。
編寫程式碼:
@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 "測試斷路器異常重試以及斷路器級別"() {
given: "設定 testService 的例項都是正常例項"
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))
when: "斷路器開啟"
//清除斷路器影響
circuitBreakerRegistry.getAllCircuitBreakers().forEach({ c -> c.reset() })
loadBalancerClientFactoryInstance = (RoundRobinWithRequestSeparatedPositionLoadBalancer) loadBalancerClientFactory.getInstance("testService")
def breaker
try {
breaker = circuitBreakerRegistry.circuitBreaker("httpbin.org:80/anything", "testService")
} catch (ConfigurationNotFoundException e) {
breaker = circuitBreakerRegistry.circuitBreaker("httpbin.org:80/anything")
}
//開啟例項 3 的斷路器
breaker.transitionToOpenState()
//呼叫 10 次
for (i in 0..<10) {
Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testService")
.get().uri("/anything").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
}
then:"呼叫至少 10 次負載均衡器且沒有異常即成功"
(10.._) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "呼叫不同的路徑,驗證斷路器在這個路徑上都是關閉"
//呼叫 10 次
for (i in 0..<10) {
Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testService")
.get().uri("/status/200").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
}
then: "呼叫必須為正好 10 次代表沒有重試,一次成功,斷路器之間相互隔離"
10 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}
測試針對 connectTimeout 重試
對於連線超時,我們需要驗證:無論是否可以重試的方法或者路徑,都必須重試,因為請求並沒有真的發出去。可以這樣驗證:設定微服務 testServiceWithCannotConnect 一個例項正常,另一個例項會連線超時,我們配置了重試 3 次,所以每次請求應該都能成功,並且隨著程式執行,後面的呼叫不可用的例項還會被斷路,照樣可以成功呼叫。
@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 "測試針對 connectTimeout 重試"() {
given: "設定微服務 testServiceWithCannotConnect 一個例項正常,另一個例項會連線超時"
loadBalancerClientFactory.getInstance("testServiceWithCannotConnect") >> loadBalancerClientFactoryInstance
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance2))
when:
//由於我們針對 testService 返回了兩個例項,一個可以正常連線,一個不可以,但是我們配置了重試 3 次,所以每次請求應該都能成功,並且隨著程式執行,後面的呼叫不可用的例項還會被斷路
//這裡主要測試針對 connect time out 還有 斷路器開啟的情況都會重試,並且無論是 GET 方法還是其他的
Span span = tracer.nextSpan()
for (i in 0..<10) {
Tracer.SpanInScope cleared = tracer.withSpanInScope(span)
try {
//測試 get 方法(預設 get 方法會重試)
Mono<String> stringMono = webClientNamedContextFactory.getWebClient("testServiceWithCannotConnect")
.get().uri("/anything").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
//測試 post 方法(預設 post 方法針對請求已經發出的不會重試,這裡沒有發出請求所以還是會重試的)
stringMono = webClientNamedContextFactory.getWebClient("testServiceWithCannotConnect")
.post().uri("/anything").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
}
finally {
cleared.close()
}
}
then:"呼叫至少 20 次負載均衡器且沒有異常即成功"
(20.._) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}
微信搜尋“我的程式設計喵”關注公眾號,每日一刷,輕鬆提升技術,斬獲各種offer: