<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          接口用例自動回歸實(shí)踐

          共 8057字,需瀏覽 17分鐘

           ·

          2022-03-15 16:01

          需 求 背 景


          在轉(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é)一下:

          技 術(shù) 實(shí) 現(xiàn)



          代碼結(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、記得定時刪除測試報告,避免磁盤被過期資源占用。


          end


          瀏覽 35
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人免费视频 国产免费麻豆 | 久久久久久99精品无码 | 一本色道久久88亚洲综合加勒比 | 挨操成人免费视频 | 国产精品偷窥熟女精品视 |