SpringCloud升級(jí)之路2020.0.x版-40.

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

我們來測試下前面封裝好的 WebClient,這里開始,我們使用 spock 編寫 groovy 單元測試,這種編寫出來的單元測試,代碼更加簡潔,同時(shí)更加靈活,我們在接下來的單元測試代碼中就能看出來。

編寫基于 spock 的 spring-boot context 測試

我們加入前面設(shè)計(jì)的配置,編寫測試類:

@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",
//因?yàn)橹卦囀?3 次,為了防止斷路器打開影響測試,設(shè)置為正好比重試多一次的次數(shù),防止觸發(fā)
//同時(shí)我們在測試的時(shí)候也需要手動(dòng)清空斷路器統(tǒng)計(jì)
"resilience4j.circuitbreaker.configs.default.minimumNumberOfCalls=4",
"resilience4j.circuitbreaker.configs.default.recordExceptions=java.lang.Exception"
],
classes = MockConfig
)
class WebClientUnitTest extends Specification {
@SpringBootApplication
static class MockConfig {
}
}

我們加入三個(gè)服務(wù)實(shí)例供單元測試調(diào)用:

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")))
}

我們要?jiǎng)討B(tài)的指定負(fù)載均衡獲取服務(wù)實(shí)例列表的響應(yīng),即去 Mock 負(fù)載均衡器的 ServiceInstanceListSupplier 并覆蓋:

class WebClientUnitTest extends Specification {

@Autowired
private Tracer tracer
@Autowired
private ServiceInstanceMetrics serviceInstanceMetrics

RoundRobinWithRequestSeparatedPositionLoadBalancer loadBalancerClientFactoryInstance = Spy();
ServiceInstanceListSupplier serviceInstanceListSupplier = Spy();

//所有測試的方法執(zhí)行前會(huì)調(diào)用的方法
def setup() {
//初始化 loadBalancerClientFactoryInstance 負(fù)載均衡器
loadBalancerClientFactoryInstance.setTracer(tracer)
loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
}
}

之后,我們可以通過下面的 groovy 代碼,動(dòng)態(tài)指定微服務(wù)返回實(shí)例:

//指定 testService 微服務(wù)的 LoadBalancer 為 loadBalancerClientFactoryInstance
loadBalancerClientFactory.getInstance("testService") >> loadBalancerClientFactoryInstance
//指定 testService 微服務(wù)實(shí)例列表為 zone1Instance1, zone1Instance3
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance3))

測試斷路器異常重試以及斷路器級(jí)別

我們需要驗(yàn)證:

  • 對(duì)于斷路器打開的異常,由于沒有請(qǐng)求發(fā)出去,所以需要直接重試其他的實(shí)例。我們可以設(shè)立一個(gè)微服務(wù),包含兩個(gè)實(shí)例,將其中一個(gè)實(shí)例的某個(gè)路徑斷路器打開,之后多次調(diào)用這個(gè)微服務(wù)的這個(gè)路徑接口,看是否都調(diào)用成功(由于有重試,所以每次調(diào)用都會(huì)成功)。同時(shí)驗(yàn)證,對(duì)于負(fù)載均衡器獲取服務(wù)實(shí)例的調(diào)用,多于調(diào)用次數(shù)(每次重試都會(huì)調(diào)用負(fù)載均衡器獲取一個(gè)新的實(shí)例用于調(diào)用)
  • 某個(gè)路徑斷路器打開的時(shí)候,其他路徑斷路器不會(huì)打開。在上面打開一個(gè)微服務(wù)某個(gè)實(shí)例的一個(gè)路徑的斷路器之后,我們調(diào)用其他的路徑,無論多少次,都成功并且調(diào)用負(fù)載均衡器獲取服務(wù)實(shí)例的次數(shù)等于調(diào)用次數(shù),代表沒有重試,也就是沒有斷路器異常。

編寫代碼:

@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",
//因?yàn)橹卦囀?3 次,為了防止斷路器打開影響測試,設(shè)置為正好比重試多一次的次數(shù),防止觸發(fā)
//同時(shí)我們在測試的時(shí)候也需要手動(dòng)清空斷路器統(tǒng)計(jì)
"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

//不同的測試方法的類對(duì)象不是同一個(gè)對(duì)象,會(huì)重新生成,保證互相沒有影響
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();

//所有測試的方法執(zhí)行前會(huì)調(diào)用的方法
def setup() {
//初始化 loadBalancerClientFactoryInstance 負(fù)載均衡器
loadBalancerClientFactoryInstance.setTracer(tracer)
loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
}

def "測試斷路器異常重試以及斷路器級(jí)別"() {
given: "設(shè)置 testService 的實(shí)例都是正常實(shí)例"
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")
}
//打開實(shí)例 3 的斷路器
breaker.transitionToOpenState()
//調(diào)用 10 次
for (i in 0..<10) {
Mono stringMono = webClientNamedContextFactory.getWebClient("testService")
.get().uri("/anything").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
}
then:"調(diào)用至少 10 次負(fù)載均衡器且沒有異常即成功"
(10.._) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
when: "調(diào)用不同的路徑,驗(yàn)證斷路器在這個(gè)路徑上都是關(guān)閉"
//調(diào)用 10 次
for (i in 0..<10) {
Mono stringMono = webClientNamedContextFactory.getWebClient("testService")
.get().uri("/status/200").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
}
then: "調(diào)用必須為正好 10 次代表沒有重試,一次成功,斷路器之間相互隔離"
10 * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}

測試針對(duì) connectTimeout 重試

對(duì)于連接超時(shí),我們需要驗(yàn)證:無論是否可以重試的方法或者路徑,都必須重試,因?yàn)檎?qǐng)求并沒有真的發(fā)出去。可以這樣驗(yàn)證:設(shè)置微服務(wù) testServiceWithCannotConnect 一個(gè)實(shí)例正常,另一個(gè)實(shí)例會(huì)連接超時(shí),我們配置了重試 3 次,所以每次請(qǐng)求應(yīng)該都能成功,并且隨著程序運(yùn)行,后面的調(diào)用不可用的實(shí)例還會(huì)被斷路,照樣可以成功調(diào)用。

@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",
//因?yàn)橹卦囀?3 次,為了防止斷路器打開影響測試,設(shè)置為正好比重試多一次的次數(shù),防止觸發(fā)
//同時(shí)我們在測試的時(shí)候也需要手動(dòng)清空斷路器統(tǒng)計(jì)
"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

//不同的測試方法的類對(duì)象不是同一個(gè)對(duì)象,會(huì)重新生成,保證互相沒有影響
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();

//所有測試的方法執(zhí)行前會(huì)調(diào)用的方法
def setup() {
//初始化 loadBalancerClientFactoryInstance 負(fù)載均衡器
loadBalancerClientFactoryInstance.setTracer(tracer)
loadBalancerClientFactoryInstance.setServiceInstanceMetrics(serviceInstanceMetrics)
loadBalancerClientFactoryInstance.setServiceInstanceListSupplier(serviceInstanceListSupplier)
}

def "測試針對(duì) connectTimeout 重試"() {
given: "設(shè)置微服務(wù) testServiceWithCannotConnect 一個(gè)實(shí)例正常,另一個(gè)實(shí)例會(huì)連接超時(shí)"
loadBalancerClientFactory.getInstance("testServiceWithCannotConnect") >> loadBalancerClientFactoryInstance
serviceInstanceListSupplier.get() >> Flux.just(Lists.newArrayList(zone1Instance1, zone1Instance2))
when:
//由于我們針對(duì) testService 返回了兩個(gè)實(shí)例,一個(gè)可以正常連接,一個(gè)不可以,但是我們配置了重試 3 次,所以每次請(qǐng)求應(yīng)該都能成功,并且隨著程序運(yùn)行,后面的調(diào)用不可用的實(shí)例還會(huì)被斷路
//這里主要測試針對(duì) connect time out 還有 斷路器打開的情況都會(huì)重試,并且無論是 GET 方法還是其他的
Span span = tracer.nextSpan()
for (i in 0..<10) {
Tracer.SpanInScope cleared = tracer.withSpanInScope(span)
try {
//測試 get 方法(默認(rèn) get 方法會(huì)重試)
Mono stringMono = webClientNamedContextFactory.getWebClient("testServiceWithCannotConnect")
.get().uri("/anything").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
//測試 post 方法(默認(rèn) post 方法針對(duì)請(qǐng)求已經(jīng)發(fā)出的不會(huì)重試,這里沒有發(fā)出請(qǐng)求所以還是會(huì)重試的)
stringMono = webClientNamedContextFactory.getWebClient("testServiceWithCannotConnect")
.post().uri("/anything").retrieve()
.bodyToMono(String.class)
println(stringMono.block())
}
finally {
cleared.close()
}
}
then:"調(diào)用至少 20 次負(fù)載均衡器且沒有異常即成功"
(20.._) * loadBalancerClientFactoryInstance.getInstanceResponseByRoundRobin(*_)
}
}

微信搜索“我的編程喵”關(guān)注公眾號(hào),每日一刷,輕松提升技術(shù),斬獲各種offer

SpringCloud升級(jí)之路2020.0.x版-40.