分布式系统接口用例自动回归实践

需 求 背 景

在转转,接口测试分为简单的单接口测试和复杂的业务场景测试。

  • 单接口测试一般在接口测试平台直接配置

  • 复杂的场景测试则需要QA另起工程自己开发

但由于测试环境的IP地址是动态分配的,以及转转RPC架构的服务调用配置方式不够灵活,QA的接口用例工程只能发挥新接口"测试"和定时在稳定环境执行的"监控"作用。缺少服务有改动部署时自动"回归"的能力。

为了能让我们的接口用例发挥更大的作用,能对服务改动做出及时响应,就需要一个在服务部署结束后自动执行接口用例的能力。

需 求 分 析

服务部署结束后自动执行测试用例,要求服务有以下能力:

1、知道服务什么时候部署结束,经过调研,beetle在服务部署结束后会发送部署成功Mq

2、监听到部署通过mq后,执行用例。

执行用例有两种方式:

  • 直接在代码里调用TestNG执行本工程写的测试用例。

  • 将接口用例拉取到本地,编译后通过命令行调用TestNG执行用例。

转转QA的用例工程一般都是数据构造和接口用例一体,本身就是一个可启动集群,自身有可监听mq能力。

第二种方式需要固定拉取分支,不利于开发,且需要额外拉取一份代码,编译后才能执行,资源浪费,且效率低。因此采用第一种。

3、服务测试环境是动态分配的,在收到mq之后才知道具体部署哪个ip,因此需要动态请求服务不同节点的能力。

4、执行结束之后需要及时通知开发和测试执行结果。

总结一下:

技 术 实 现

代码结构

上面说过需要在代码里面调用TestNG,因此要将接口用例和数据构造代码放在一起。方便TestNG调用。

 
  1. ├── contract                               // 数据构造接口定义

  2. └── service

  3.     └── src.main.java

  4.         └── com.zhuanzhuan.mpqa

  5.            ├── Boot.java                   // 启动服务

  6.            ├── component                   // 数据构造接口实现

  7.            ├── system                      // 自动注入RPC接口bean

  8.                ├── RpcProxyHandler.java    // RpcProxyHandler        

  9.                ├── RpcBeanRegistry.java    // RpcBeanRegistry

  10.                ├── MqComsumer.java         // Mq消费者

  11.                ├── TestNGSpringContext.java

  12.                ├── TestContextManager.java

  13.            ├── wrapper                     // 三方接口封装

  14.            └── zztest                      // 用例目录

  15.                ├── BaseTest.java           // 本地测试时,初始化spring依赖

  16.                ├── TestNGHelper.class 

  17.                ├── case                    // 用例

部署成功mq

MqComsumer.java

 
  1. @Component

  2. public class MqComsumer {

  3.     @ZZMQListener(group = "Consumer", subscribe = @Subscribe(topic = "deploySuccessTopic"))

  4.     public void beetleDeploy(@Body List<AutoRunCases> beetleDeploys) {

  5.         AutoRunCases beetleDeploy = beetleDeploys.get(0);

  6.         TestNGHelper.run(beetleDeploy.getCluster(), beetleDeploy.getIp());

  7.         sendResult();

  8.     }

  9. }

代码调用TestNG

TestNGHelper.class

 
  1. public class TestNGHelper {

  2.     public static boolean run(String serviceName, String ip) {

  3.       

  4.         // 获取服务配置的用例

  5.         List<Case> cases = caseConfigMap.get(serviceName);

  6.         // suit

  7.         XmlSuite xmlSuite = new XmlSuite();

  8.         xmlSuite.setName(serviceName + "#" + ip);

  9.         Map<String,String> parameters = new HashMap<>();

  10.         // 这里将ip传入TestNG

  11.         parameters.put("ip", ip);

  12.         xmlSuite.setParameters(parameters);

  13.         // test

  14.         XmlTest xmlTest = new XmlTest(xmlSuite);

  15.         // classes

  16.         List<XmlClass> classes = new ArrayList<>();

  17.         cases.forEach(testCase -> {

  18.             XmlClass xmlClass = new XmlClass(testCase.getClazz());

  19.             classes.add(xmlClass);

  20.             // include

  21.             List<XmlInclude> xmlIncludes = new ArrayList<>();

  22.             testCase.getMethods().forEach(method -> {

  23.                 XmlInclude xmlInclude = new XmlInclude(method);

  24.                 xmlIncludes.add(xmlInclude);

  25.             });

  26.             xmlClass.setIncludedMethods(xmlIncludes);

  27.         });

  28.         xmlTest.setXmlClasses(classes);

  29.         TestNG testNG = new TestNG();

  30.         List<XmlSuite> suites = new ArrayList<>();

  31.         suites.add(xmlSuite);

  32.         testNG.setXmlSuites(suites);

  33.        testNG.setOutputDirectory("/home/work/test_report");

  34.         testNG.run();

  35.         return true;

  36.     }

  37. }

注意:这里需要通过xmlSuite.setParameter传递IP地址

这里直接run的话,会有一个坑,后面会讲到。

动态调用服务不同节点(ip)

转转的RPC框架提供了两种不同的初始化方式。XML和API。

XML配置时,ip信息是写死的,不符合我们的需求。因此需要采用api调用的

方式。ip通过之前TestNG的XmlSuite.setParameters获取。

这种方式,每添加一个接口,都需要手写一个bean,不够优雅。为了能够简化用例编写和减少代码冗余,我们可以实现一个BeanDefinitionRegistryPostProcessor统一处理。后续调用可以跟其他Bean一样,直接@Resoures或者@Autowired即可。

BeanDefinitionRegistryPostProcessor 和FactoryBean

RpcBeanRegistry.java

 
  1. @Component

  2. public class RpcBeanRegistry implements BeanDefinitionRegistryPostProcessor {

  3.     private static final String MP_PACKAGE = "com.zhuanzhuan.mpqa";

  4.     @Override

  5.     @PostConstruct

  6.     public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {

  7.         scanResourceScfContract().forEach(contract -> {

  8.             // 生成BeanDefinition

  9.             BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(RpcBeanFactory.class);

  10.             // 解析后注入 registry  即:  beanDefinitionMap.put (beanName, beanDefinition);

  11.             AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();

  12.             // 注入属性

  13.             beanDefinition.getPropertyValues().add("contract", contract);

  14.             // 自定义 beanDefinition

  15.             String beanName = contract.getName() + "$ByScfBeanRegistry";

  16.             beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinition);

  17.         });

  18.     }

  19.     @Override

  20.     public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {

  21.     }

  22.     /**

  23.      * 扫描有@Resource 和 @Autowired的field, 并判断是否是接口

  24.      */

  25.     private Set<Class<?>> scanResourceRpcContract() {

  26.         Set<Class<?>> classes = ClassScanner.scanPackage(MP_PACKAGE);

  27.         Set<Class<?>> contractBean = new HashSet<>();

  28.         classes.forEach(clazz -> {

  29.             Field[] fields = clazz.getDeclaredFields();

  30.             Arrays.asList(fields).forEach(field -> {

  31.                 Annotation resource = field.getDeclaredAnnotation(Resource.class);

  32.                 Annotation autoWire = field.getDeclaredAnnotation(Autowired.class);

  33.                 if(resource == null && autoWire == null) {

  34.                     return;

  35.                 }

  36.                 Class<?> type = field.getType();

  37.                 if(!type.isInterface()) {

  38.                     return;

  39.                 }

  40.                 // 当前package

  41.                 if(type.getName().startsWith(MP_PACKAGE)) {

  42.                     return;

  43.                 }

  44.                 if(type.getAnnotation(ServiceContract.class) == null) {

  45.                     return;

  46.                 }

  47.                 contractBean.add(type);

  48.             });

  49.         });

  50.         return contractBean;

  51.     }

  52. }

  53. @Setter

  54. class RpcBeanFactory implements FactoryBean<Object> {

  55.     private Class<?> contract;

  56.     @Override

  57.     public Object getObject() {

  58.         ScfProxyHandler handler = new RpcProxyHandler(contract);

  59.         return handler.getProxy();

  60.     }

  61.     @Override

  62.     public Class<?> getObjectType() {

  63.         return contract;

  64.     }

  65.     @Override

  66.     public boolean isSingleton() {

  67.         return true;

  68.     }

  69. }

InvocationHandler

ScfProxyHandler.java

 
  1. public class ScfProxyHandler implements InvocationHandler {

  2.     private static final int SCF_TIMEOUT = 200000;

  3.     private Class<?> contract;

  4.     public ScfProxyHandler(Class<?> contract) {

  5.         this.contract = contract;

  6.     }

  7.     public Object getProxy() {

  8.         return Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] {contract}, this);

  9.     }

  10.     @Override

  11.     public Object invoke(Object proxy, Method method, Object[] args) throws Exception {

  12.         String methodName = method.getName();

  13.         ReferenceArgs referenceArgs = new ReferenceArgs(contract);

  14.         ApplicationConfig applicationConfig = SpringContext.getApplicationContext().getBean(ApplicationConfig.class);

  15.         ServiceReferenceConfig serviceReferenceConfig = new ServiceReferenceConfig();

  16.         serviceReferenceConfig.setServiceName(referenceArgs.getServiceName());

  17.         serviceReferenceConfig.setServiceRpcArgs(new ServiceRpcArgs());

  18.         serviceReferenceConfig.getServiceRpcArgs().setTimeout(SCF_TIMEOUT);

  19.         ServerNode serverNode = new ServerNode();

  20.         // 获取当前suite试用的ip

  21.         String ip = Reporter.getCurrentTestResult().getTestContext().getSuite().getParameter("ip");

  22.         serverNode.setHost(ip);

  23.         serverNode.setPort(referenceArgs.getTcpPort());

  24.         serviceReferenceConfig.setServerNodes(Collections.singletonList(serverNode));

  25.         Object refer = new Reference.ReferenceBuilder<>()

  26.                 .applicationConfig(applicationConfig)

  27.                 .interfaceName(contract.getName())

  28.                 .serviceName(referenceArgs.getServiceName())

  29.                 .localReferenceConfig(serviceReferenceConfig)

  30.                 .build()

  31.                 .refer();

  32.         return method.invoke(refer, args);

  33.     }

  34. }

输出测试报告

执行结束之后会在设置 testNG.setOutputDirectory("/home/work/test_report") 的目录/home/work/test_report中生成测试报告。如果你是web服务,可以直接通过企业微信群发或者发送告警消息,如果是其他服务可以发送邮件。默认的报告不太美观,可以使用其他插件优化。

整体流程

踩 坑

application has already bean instanced

前面说过,直接调用TestNG.run会有坑坑就是TestNG本身无法在已经启动的spring实例中执行😭。原因是:在服务启动的时候,实例已经启动,相关的依赖已经注入,而TestNG在执行用例前会再次注入依赖。

经过查看TestNG启动的源码,梳理出TestNG的启动调用链和注入依赖的代码如下:

在这四个AbstractTestExecutionListener中的

DependencyInjectionTestExecutionListener是负责依赖注入的,而且

AbstractTestNGSpringContextTests和TestContextManager是比较独立

的,因此我们可以切个"分支"(重写AbstractTestNGSpringContextTests和

TestContextManager)。

步骤:

1、复制一份org.springframework.test.context.TestContextManager

添加以下判断

2、复制一份

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

其他问题

在实际操作中,还有许多需要注意的地方:

1、多服务时,如何维护稳定节点和动态节点。需要维护三套环境:稳定环

境、动态测试环境和执行用例的环境,通过区分使用请求归属,从而决定使用

哪个ip。或者通过流量路由标签设置也可以。

2、用例服务多节点时,如何处理并发。需要在监听mq部分加上分布式锁和

幂等校验

3、区分数据构造请求和用例执行请求。TestNGContext.getTestContext()

== null ? 数据构造请求 : 用例请求。

4、记得定时删除测试报告,避免磁盘被过期资源占用。

感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你!有需要的小伙伴可以点击下方小卡片领取   

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值