写点什么

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

  • 2022 年 8 月 22 日
    北京
  • 本文字数:5067 字

    阅读完需:约 17 分钟

需 求 背 景


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


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


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


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


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


需 求 分 析


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


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


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


执行用例有两种方式:


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


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


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


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


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


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


总结一下:图片


技 术 实 现


代码结构


上面说过需要在代码里面调用 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 // 用例


部署成功 mqMqComsumer.java


@Componentpublic 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();}
复制代码


}


代码调用 TestNGTestNGHelper.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 获取。


图片


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


BeanDefinitionRegistryPostProcessor 和 FactoryBean


RpcBeanRegistry.java


@Componentpublic class RpcBeanRegistry implements BeanDefinitionRegistryPostProcessor {


private static final String MP_PACKAGE = "com.zhuanzhuan.mpqa";
@Override@PostConstructpublic 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); });}
@Overridepublic 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;}
复制代码


}


@Setterclass RpcBeanFactory implements FactoryBean<Object> {


private Class<?> contract;
@Overridepublic Object getObject() { ScfProxyHandler handler = new RpcProxyHandler(contract); return handler.getProxy();}
@Overridepublic Class<?> getObjectType() { return contract;}
@Overridepublic boolean isSingleton() { return true;}
复制代码


}


InvocationHandlerScfProxyHandler.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);}
@Overridepublic 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 服务,可以直接通过企业微信群发或者发送告警消息,如果是其他服务可以发送邮件。默认的报告不太美观,可以使用其他插件优化。


图片


整体流程图片


踩 坑


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、记得定时删除测试报告,避免磁盘被过期资源占用。


作者:陈俊华


转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。


关注公众号「转转技术」(综合性)、「大转转 FE」(专注于 FE)、「转转 QA」(专注于 QA),更多干货实践,欢迎交流分享~

发布于: 刚刚阅读数: 3
用户头像

还未添加个人签名 2019.04.30 加入

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」,各种干货实践,欢迎交流分享~

评论

发布
暂无评论
分布式系统接口用例自动回归实践_接口测试_转转技术团队_InfoQ写作社区