接口用例自動回歸實(shí)踐
在轉(zhuǎn)轉(zhuǎn),接口測試分為簡單的單接口測試和復(fù)雜的業(yè)務(wù)場景測試。
單接口測試一般在接口測試平臺直接配置
復(fù)雜的場景測試則需要QA另起工程自己開發(fā)
但由于測試環(huán)境的IP地址是動態(tài)分配的,以及轉(zhuǎn)轉(zhuǎn)RPC架構(gòu)的服務(wù)調(diào)用配置方式不夠靈活,QA的接口用例工程只能發(fā)揮新接口"測試"和定時在穩(wěn)定環(huán)境執(zhí)行的"監(jiān)控"作用。缺少服務(wù)有改動部署時自動"回歸"的能力。
為了能讓我們的接口用例發(fā)揮更大的作用,能對服務(wù)改動做出及時響應(yīng),就需要一個在服務(wù)部署結(jié)束后自動執(zhí)行接口用例的能力。
服務(wù)部署結(jié)束后自動執(zhí)行測試用例,要求服務(wù)有以下能力:
1、知道服務(wù)什么時候部署結(jié)束,經(jīng)過調(diào)研,beetle在服務(wù)部署結(jié)束后會發(fā)送部署成功Mq。
2、監(jiān)聽到部署通過mq后,執(zhí)行用例。
執(zhí)行用例有兩種方式:
直接在代碼里調(diào)用TestNG執(zhí)行本工程寫的測試用例。
將接口用例拉取到本地,編譯后通過命令行調(diào)用TestNG執(zhí)行用例。
用例工程一般都是數(shù)據(jù)構(gòu)造和接口用例一體,本身就是一個可啟動集群,自身有可監(jiān)聽mq能力。
第二種方式需要固定拉取分支,不利于開發(fā),且需要額外拉取一份代碼,編譯后才能執(zhí)行,資源浪費(fèi),且效率低。因此采用第一種。
3、服務(wù)測試環(huán)境是動態(tài)分配的,在收到mq之后才知道具體部署哪個ip,因此需要動態(tài)請求服務(wù)不同節(jié)點(diǎn)的能力。
4、執(zhí)行結(jié)束之后需要及時通知開發(fā)和測試執(zhí)行結(jié)果。
總結(jié)一下:
代碼結(jié)構(gòu)
上面說過需要在代碼里面調(diào)用TestNG,因此要將接口用例和數(shù)據(jù)構(gòu)造代碼放在一起。方便TestNG調(diào)用。
├──?contract???????????????????????????????//?數(shù)據(jù)構(gòu)造接口定義
└──?service
????└──?src.main.java
????????└──?com.zhuanzhuan.mpqa
???????????├──?Boot.java???????????????????//?啟動服務(wù)
???????????├──?component???????????????????//?數(shù)據(jù)構(gòu)造接口實(shí)現(xiàn)
???????????├──?system??????????????????????//?自動注入RPC接口bean
???????????????├──?RpcProxyHandler.java????//?RpcProxyHandler????????
???????????????├──?RpcBeanRegistry.java????//?RpcBeanRegistry
???????????????├──?MqComsumer.java?????????//?Mq消費(fèi)者
???????????????├──?TestNGSpringContext.java
???????????????├──?TestContextManager.java
???????????├──?wrapper?????????????????????//?三方接口封裝
???????????└──?zztest??????????????????????//?用例目錄
???????????????├──?BaseTest.java???????????//?本地測試時,初始化spring依賴
???????????????├──?TestNGHelper.class?
???????????????├──?case????????????????????//?用例
部署成功mq
MqComsumer.java
@Component
public?class?MqComsumer?{
????@ZZMQListener(group?=?"Consumer",?subscribe?=?@Subscribe(topic?=?"deploySuccessTopic"))
????public?void?beetleDeploy(@Body?List?beetleDeploys) ?{
????????AutoRunCases?beetleDeploy?=?beetleDeploys.get(0);
????????TestNGHelper.run(beetleDeploy.getCluster(),?beetleDeploy.getIp());
????????sendResult();
????}
}代碼調(diào)用TestNG
TestNGHelper.class
public?class?TestNGHelper?{
????public?static?boolean?run(String?serviceName,?String?ip)?{
??????
????????//?獲取服務(wù)配置的用例
????????List?cases?=?caseConfigMap.get(serviceName);
????????//?suit
????????XmlSuite?xmlSuite?=?new?XmlSuite();
????????xmlSuite.setName(serviceName?+?"#"?+?ip);
????????Map?parameters?=?new?HashMap<>();
????????//?這里將ip傳入TestNG
????????parameters.put("ip",?ip);
????????xmlSuite.setParameters(parameters);
????????//?test
????????XmlTest?xmlTest?=?new?XmlTest(xmlSuite);
????????//?classes
????????List?classes?=?new?ArrayList<>();
????????cases.forEach(testCase?->?{
????????????XmlClass?xmlClass?=?new?XmlClass(testCase.getClazz());
????????????classes.add(xmlClass);
????????????//?include
????????????List?xmlIncludes?=?new?ArrayList<>();
????????????testCase.getMethods().forEach(method?->?{
????????????????XmlInclude?xmlInclude?=?new?XmlInclude(method);
????????????????xmlIncludes.add(xmlInclude);
????????????});
????????????xmlClass.setIncludedMethods(xmlIncludes);
????????});
????????xmlTest.setXmlClasses(classes);
????????TestNG?testNG?=?new?TestNG();
????????List?suites?=?new?ArrayList<>();
????????suites.add(xmlSuite);
????????testNG.setXmlSuites(suites);
???????testNG.setOutputDirectory("/home/work/test_report");
????????testNG.run();
????????return?true;
????}
} 注意:這里需要通過xmlSuite.setParameter傳遞IP地址
這里直接run的話,會有一個坑,后面會講到。
動態(tài)調(diào)用服務(wù)不同節(jié)點(diǎn)(ip)
轉(zhuǎn)轉(zhuǎn)的RPC框架提供了兩種不同的初始化方式。XML和API。
XML配置時,ip信息是寫死的,不符合我們的需求。因此需要采用api調(diào)用的
方式。ip通過之前TestNG的XmlSuite.setParameters獲取。

這種方式,每添加一個接口,都需要手寫一個bean,不夠優(yōu)雅。為了能夠簡化用例編寫和減少代碼冗余,我們可以實(shí)現(xiàn)一個BeanDefinitionRegistryPostProcessor統(tǒng)一處理。后續(xù)調(diào)用可以跟其他Bean一樣,直接@Resoures或者@Autowired即可。
BeanDefinitionRegistryPostProcessor 和FactoryBean
RpcBeanRegistry.java
@Component
public?class?RpcBeanRegistry?implements?BeanDefinitionRegistryPostProcessor?{
????private?static?final?String?MP_PACKAGE?=?"com.zhuanzhuan.mpqa";
????@Override
????@PostConstruct
????public?void?postProcessBeanDefinitionRegistry(BeanDefinitionRegistry?beanDefinitionRegistry)?throws?BeansException?{
????????scanResourceScfContract().forEach(contract?->?{
????????????//?生成BeanDefinition
????????????BeanDefinitionBuilder?builder?=?BeanDefinitionBuilder.genericBeanDefinition(RpcBeanFactory.class);
????????????//?解析后注入?registry??即:??beanDefinitionMap.put?(beanName,?beanDefinition);
????????????AbstractBeanDefinition?beanDefinition?=?builder.getBeanDefinition();
????????????//?注入屬性
????????????beanDefinition.getPropertyValues().add("contract",?contract);
????????????//?自定義?beanDefinition
????????????String?beanName?=?contract.getName()?+?"$ByScfBeanRegistry";
????????????beanDefinitionRegistry.registerBeanDefinition(beanName,?beanDefinition);
????????});
????}
????@Override
????public?void?postProcessBeanFactory(ConfigurableListableBeanFactory?configurableListableBeanFactory)?throws?BeansException?{
????}
????/**
?????*?掃描有@Resource?和?@Autowired的field,?并判斷是否是接口
?????*/
????private?Set>?scanResourceRpcContract()?{
????????Set>?classes?=?ClassScanner.scanPackage(MP_PACKAGE);
????????Set>?contractBean?=?new?HashSet<>();
????????classes.forEach(clazz?->?{
????????????Field[]?fields?=?clazz.getDeclaredFields();
????????????Arrays.asList(fields).forEach(field?->?{
????????????????Annotation?resource?=?field.getDeclaredAnnotation(Resource.class);
????????????????Annotation?autoWire?=?field.getDeclaredAnnotation(Autowired.class);
????????????????if(resource?==?null?&&?autoWire?==?null)?{
????????????????????return;
????????????????}
????????????????Class>?type?=?field.getType();
????????????????if(!type.isInterface())?{
????????????????????return;
????????????????}
????????????????//?當(dāng)前package
????????????????if(type.getName().startsWith(MP_PACKAGE))?{
????????????????????return;
????????????????}
????????????????if(type.getAnnotation(ServiceContract.class)?==?null)?{
????????????????????return;
????????????????}
????????????????contractBean.add(type);
????????????});
????????});
????????return?contractBean;
????}
}
@Setter
class?RpcBeanFactory?implements?FactoryBean<Object>?{
????private?Class>?contract;
????@Override
????public?Object?getObject()?{
????????ScfProxyHandler?handler?=?new?RpcProxyHandler(contract);
????????return?handler.getProxy();
????}
????@Override
????public?Class>?getObjectType()?{
????????return?contract;
????}
????@Override
????public?boolean?isSingleton()?{
????????return?true;
????}
} InvocationHandler
ScfProxyHandler.java
public?class?ScfProxyHandler?implements?InvocationHandler?{
????private?static?final?int?SCF_TIMEOUT?=?200000;
????private?Class>?contract;
????public?ScfProxyHandler(Class>?contract)?{
????????this.contract?=?contract;
????}
????public?Object?getProxy()?{
????????return?Proxy.newProxyInstance(this.getClass().getClassLoader(),?new?Class[]?{contract},?this);
????}
????@Override
????public?Object?invoke(Object?proxy,?Method?method,?Object[]?args)?throws?Exception?{
????????String?methodName?=?method.getName();
????????ReferenceArgs?referenceArgs?=?new?ReferenceArgs(contract);
????????ApplicationConfig?applicationConfig?=?SpringContext.getApplicationContext().getBean(ApplicationConfig.class);
????????ServiceReferenceConfig?serviceReferenceConfig?=?new?ServiceReferenceConfig();
????????serviceReferenceConfig.setServiceName(referenceArgs.getServiceName());
????????serviceReferenceConfig.setServiceRpcArgs(new?ServiceRpcArgs());
????????serviceReferenceConfig.getServiceRpcArgs().setTimeout(SCF_TIMEOUT);
????????ServerNode?serverNode?=?new?ServerNode();
????????//?獲取當(dāng)前suite試用的ip
????????String?ip?=?Reporter.getCurrentTestResult().getTestContext().getSuite().getParameter("ip");
????????serverNode.setHost(ip);
????????serverNode.setPort(referenceArgs.getTcpPort());
????????serviceReferenceConfig.setServerNodes(Collections.singletonList(serverNode));
????????Object?refer?=?new?Reference.ReferenceBuilder<>()
????????????????.applicationConfig(applicationConfig)
????????????????.interfaceName(contract.getName())
????????????????.serviceName(referenceArgs.getServiceName())
????????????????.localReferenceConfig(serviceReferenceConfig)
????????????????.build()
????????????????.refer();
????????return?method.invoke(refer,?args);
????}
}輸出測試報告
執(zhí)行結(jié)束之后會在設(shè)置 testNG.setOutputDirectory("/home/work/test_report") 的目錄/home/work/test_report中生成測試報告。如果你是web服務(wù),可以直接通過企業(yè)微信群發(fā)或者發(fā)送告警消息,如果是其他服務(wù)可以發(fā)送郵件。默認(rèn)的報告不太美觀,可以使用其他插件優(yōu)化。

整體流程

application has already bean instanced

前面說過,直接調(diào)用TestNG.run會有坑。坑就是TestNG本身無法在已經(jīng)啟動的spring實(shí)例中執(zhí)行??。原因是:在服務(wù)啟動的時候,實(shí)例已經(jīng)啟動,相關(guān)的依賴已經(jīng)注入,而TestNG在執(zhí)行用例前會再次注入依賴。
經(jīng)過查看TestNG啟動的源碼,梳理出TestNG的啟動調(diào)用鏈和注入依賴的代碼如下:


在這四個AbstractTestExecutionListener中的
DependencyInjectionTestExecutionListener是負(fù)責(zé)依賴注入的,而且
AbstractTestNGSpringContextTests和TestContextManager是比較獨(dú)立
的,因此我們可以切個"分支"(重寫AbstractTestNGSpringContextTests和
TestContextManager)。

步驟:
1、復(fù)制一份org.springframework.test.context.TestContextManager
添加以下判斷

2、復(fù)制一份
org.springframework.test.context.testng.AbstractTestNGSpringContext
Tests命名為TestNGSpringContext,將import
org.springframework.test.context.TestContextManager 修改為 import
com.zhuanzhuan.mpqa.system.TestContextManager
3、BaseTest繼承com.zhuanzhuan.mpqa.system.TestNGSpringContext

其他問題
在實(shí)際操作中,還有許多需要注意的地方:
1、多服務(wù)時,如何維護(hù)穩(wěn)定節(jié)點(diǎn)和動態(tài)節(jié)點(diǎn)。需要維護(hù)三套環(huán)境:穩(wěn)定環(huán)
境、動態(tài)測試環(huán)境和執(zhí)行用例的環(huán)境,通過區(qū)分使用請求歸屬,從而決定使用
哪個ip?;蛘咄ㄟ^流量路由標(biāo)簽設(shè)置也可以。
2、用例服務(wù)多節(jié)點(diǎn)時,如何處理并發(fā)。需要在監(jiān)聽mq部分加上分布式鎖和
冪等校驗(yàn)
3、區(qū)分?jǐn)?shù)據(jù)構(gòu)造請求和用例執(zhí)行請求。TestNGContext.getTestContext()
== null ? 數(shù)據(jù)構(gòu)造請求 : 用例請求。
4、記得定時刪除測試報告,避免磁盤被過期資源占用。
