• 接口用例自动回归实践


    需 求 背 景

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

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

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

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

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

    需 求 分 析

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

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

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

    执行用例有两种方式:

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

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

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

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

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

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

    总结一下:36082d6a8a597bf501170c329572e52e.png

    技 术 实 现

    代码结构

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

    ├── contract                               // 数据构造接口定义
    └── service
        └── src.main.java
            └── com.zhuanzhuan.mpqa
               ├── Boot.java                   // 启动服务
               ├── component                   // 数据构造接口实现
               ├── system                      // 自动注入RPC接口bean
                   ├── RpcProxyHandler.java    // RpcProxyHandler        
                   ├── RpcBeanRegistry.java    // RpcBeanRegistry
                   ├── MqComsumer.java         // Mq消费者
                   ├── 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<AutoRunCases> beetleDeploys) {
            AutoRunCases beetleDeploy = beetleDeploys.get(0);
            TestNGHelper.run(beetleDeploy.getCluster(), beetleDeploy.getIp());
            sendResult();
        }
    }


    代码调用TestNG

    TestNGHelper.class

    public class TestNGHelper {
    
        public static boolean run(String serviceName, String ip) {
          
            // 获取服务配置的用例
            List<Case> cases = caseConfigMap.get(serviceName);
    
            // suit
            XmlSuite xmlSuite = new XmlSuite();
            xmlSuite.setName(serviceName + "#" + ip);
            Map<String,String> parameters = new HashMap<>();
            // 这里将ip传入TestNG
            parameters.put("ip", ip);
            xmlSuite.setParameters(parameters);
            // test
            XmlTest xmlTest = new XmlTest(xmlSuite);
            // classes
            List<XmlClass> classes = new ArrayList<>();
            cases.forEach(testCase -> {
                XmlClass xmlClass = new XmlClass(testCase.getClazz());
                classes.add(xmlClass);
                // include
                List<XmlInclude> 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<XmlSuite> suites = new ArrayList<>();
            suites.add(xmlSuite);
            testNG.setXmlSuites(suites);
           testNG.setOutputDirectory("/home/work/test_report");
            testNG.run();
            return true;
        }
    }

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

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

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

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

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

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

    daa3d6fd3da2034b6d8a768bf5afa731.png

    这种方式,每添加一个接口,都需要手写一个bean,不够优雅。为了能够简化用例编写和减少代码冗余,我们可以实现一个BeanDefinitionRegistryPostProcessor统一处理。后续调用可以跟其他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<Class<?>> scanResourceRpcContract() {
            Set<Class<?>> classes = ClassScanner.scanPackage(MP_PACKAGE);
            Set<Class<?>> 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;
                    }
                    // 当前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();
            // 获取当前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);
        }
    }


    输出测试报告

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

    3360a1a7a64a6ed1027c65df940ec67a.png

    整体流程

    7460916428a9273e8a767f474c979858.png

    踩 坑

    application has already bean instanced

    b1c5b16812e33c8033c5aa4a8ba404c5.png

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

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

    bf2ec6be8ea43385677d03f6329a32a7.png

    221e57b7d102c7e919fb16804d988c2b.png

    在这四个AbstractTestExecutionListener中的

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

    AbstractTestNGSpringContextTests和TestContextManager是比较独立

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

    TestContextManager)。

    7cd1f34b9d4e130a8b12619e6b15eee6.png

    步骤:

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

    添加以下判断

    99045e347029aafcab3f2790176e7480.png

    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

    a4b687df556a03d64a11ba326b47fc3d.png

    其他问题

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

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

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

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

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

    幂等校验

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

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

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

    end

  • 相关阅读:
    spring学习(一)--spring简介
    The Evolved Packet Core
    Git版本控制入门学习
    分家后中国移动运营商2G/3G频率分配
    English name
    中国互联网五大势力竞争力报告
    ping
    mysql架构
    MySQL存储引擎
    79款 C/C++开发工具开源软件
  • 原文地址:https://www.cnblogs.com/finer/p/16157211.html
Copyright © 2020-2023  润新知