SpringCloud升級之路2020.0.x版-40.

本系列代碼地址:??https://github.com/JoJoTec/spring-cloud-parent??

我們來測試下前面封裝好的 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 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 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 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

SpringCloud升級之路2020.0.x版-40.