自动化测试如何访问不同的环境对应服务实例

首先介绍做一下场景介绍:

  1、我们公司的测试环境比较复杂,预发环境(UAT)一套,SIT环境4套,DEV环境7套。我是负责中台模块的测试,功能类似一个订单中心,但是功能相对比较复杂。网关进来的95%以上的请求都要我负责的模块来处理(不论线上业务还是线下业务,因此所有的环境都要经过我负责模块。

     2、我们公司使用的grpc微服务框架,而我负责的中台模块,都是通过grpc的微服务接口(不提供http接口),对于测试来讲,这是个不幸的消息。

那么我们中台的接口自动化测试是如何来实现的呢?

这个是完整 RPC 架构图

一个 RPC 的核心功能主要有 5 个部分组成,分别是:客户端、客户端 Stub、网络传输模块、服务端 Stub、服务端等

  • 客户端(Client):服务调用方。
  • 客户端存根(Client Stub):存放服务端地址信息,将客户端的请求参数数据信息打包成网络消息,再通过网络传输发送给服务端。
  • 服务端存根(Server Stub):接收客户端发送过来的请求消息并进行解包,然后再调用本地服务进行处理。
  • 服务端(Server):服务的真正提供者。
  • Network Service:底层传输,可以是 TCP 或 HTTP。

了解上面的基本知识。现在来介绍我是如何实现的。

1、创建连接到远程服务器的 channel
2、构建使用该channel的客户端stub
3、调用服务方法,执行RPC调用
4、封装成Controller

 构建客户端stub

public class Client {


    //样例 stub
    private DemoServiceGrpc.DemoServiceBlockingStub demoServiceBlockingStub;
    //原生的stub 点对点测试
    public Client() {
        ManagedChannel channel = null;
        try {
            String ip =PropertiesUtils.getValue("****.grpc.ip");
            String port = PropertiesUtils.getValue("****.grpc.port");
            channel = ManagedChannelBuilder.forTarget("static://" + ip + ":" + port).usePlaintext().build();
        } catch (Exception e) {
            e.printStackTrace();
        }
        demoServiceBlockingStub = DemoServiceGrpc.newBlockingStub(channel);
        
    }
    public DemoServiceGrpc.DemoServiceBlockingStub getdemoServiceBlockingStub() {
        return demoServiceBlockingStub;
    }
}

 封装Controller:

 

那么简单的http 接口服务就实现了。接下来重点来了,如何实现部署一台服务访问不同环境呢????

具体实现:

基于spring提供原生的 AbstractRoutingDataSource ,参考一些文档自己实现切换

1、 为了区分不同环境的配置,采用了application-{}.yaml文件来隔离, 然后通过application.yaml文件来控制加载所有的配置文件

2、application.yaml配置

3、在Springboot的启动类上,排除掉datasource自动配置

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class GrpcApplication {

    public static void main(String[] args) {
        SpringApplication.run(GrpcApplication.class, args);
    }

}

4、新建一个EnvContext类,采用ThreadLocal的方式,对每个请求线程的环境变量进行隔离,这里容易遇到坑,springboot都是内嵌的tomcat启动模式,如果tomcat设置了链接的重用规则,那么如果env的信息没有被清除,可能会导致错误加载配置

/**
 * 用来存放环境的变量,用于动态的去切换
 */
public class EnvContext {
 
    public static ThreadLocal<String> envThreadLocal = new InheritableThreadLocal<>();
 
    public static String getEnv(){
        return envThreadLocal.get();
    }
 
    public static void setEnv(String env){
        envThreadLocal.set(env);
    }
 
    public static void clear(){
        envThreadLocal.remove();
    }
}

5、创建一个DynamicDataSource, 这里继承了AbstractRoutingDataSource,动态数据源类集成了Spring提供的AbstractRoutingDataSource类,AbstractRoutingDataSource 中获取数据源的方法就是 determineTargetDataSource,而此方法又通过 determineCurrentLookupKey 方法获取查询数据源的key。

public class DynamicDataSource extends AbstractRoutingDataSource {
 
 
    @Override
    protected Object determineCurrentLookupKey() {
        return EnvContext.getEnv();
    }
}

6、定义一个枚举类,放入所有的环境信息

@Getter
@AllArgsConstructor
@NoArgsConstructor
public enum EnvEnum {

    DEV1("dev1","开发环境dev1"),
    DEV2("dev2","开发环境dev2"),
    DEV3("dev3","开发环境dev3"),
    DEV4("dev4","开发环境dev4"),
    DEV5("dev5","开发环境dev5"),
    DEV6("dev6","开发环境dev6"),
    DEV7("dev7","开发环境dev7"),
    SIT1("sit1","集成环境SIT1"),
    SIT2("sit2","集成环境SIT2"),
    SIT3("sit3","集成环境SIT3"),
    SIT4("sit4","集成环境SIT4"),
    UAT("uat","集成环境UAT");
    public String env;
    public String desc;
}

7、重点来了,我们通过AOP, 去拿到每次http的请求头中的header信息,来动态的切换EnvContext中的env配置

@Aspect
@Component
@Slf4j
public class EnvAop {

    public ThreadLocal<String> threadLocal = new ThreadLocal<>();

    @Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping ) && @annotation(io.swagger.annotations.ApiOperation))")
    public void ex(){}


    @Around("ex()")
    public Object envAop(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        Object result;
        try {
            //获取每个请求的header,拿到环境变量的参数,存入ThreadLocal中,供每个线程使用
            RequestAttributes ra = RequestContextHolder.getRequestAttributes();
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            HttpServletRequest request = sra.getRequest();
            // 获取请求头
            Enumeration<String> enumeration = request.getHeaderNames();
            //
            String env = request.getHeader("env");
            if(StringUtils.isEmpty(env)){
                log.info("~~~~ 拦截到http请求,环境变量信息为空,设置为默认dev1", env);
                env = EnvEnum.DEV1.env;
            }
            log.info("~~~~ 拦截到http请求,环境变量信息为{}", env);
            EnvConfig.envThreadLocal.set(env);
            result = proceedingJoinPoint.proceed();

        } finally {
            //请求结束后,将环境变量的信息从ThreadLocal中移除
            EnvConfig.clear();
            log.info("~~~~ http请求结束,重置env的信息为{}" , EnvConfig.getEnv());
        }
        return result;
    }
}

8、编写一个工具类,动态获取Spring的容器ApplicationContext

@Component
public class SpringContextUtil {

    @Resource
    private ApplicationContext applicationContext;

    private static ConfigurableApplicationContext context;
    private static BeanFactory factoryBean;

    @PostConstruct
    public void init() {
        context = (ConfigurableApplicationContext) applicationContext;
        factoryBean = context.getBeanFactory();
    }

    public static BeanFactory getFactoryBean() {
        return factoryBean;
    }

    public static ConfigurableApplicationContext getApplicationContext() {
        return context;
    }
}

9、然后编写一个配置信息动态读取工具类,每次请求进来,env会动态切换,然后工具类会自动拼装env信息去读取

  

public class PropertiesUtils {
    public static String getValue(String key) throws Exception {
        Environment environment = (Environment) SpringContextUtil.getApplicationContext().getBean("environment");
        String value = environment.getProperty(EnvConfig.getEnv() + "." + key);
        if(StringUtils.isEmpty(value)){
            throw new Exception("配置信息获取失败,请检查application-"+ EnvConfig.getEnv()+".yaml文件!, key = " + key + " , env = " + EnvConfig.getEnv());
        }
        return value;
    }
}

到此  实现通过http请求 中header中配置env参数来实现  动态切换服务器(以此类推可以修改同过parame或者url中的参数来实现动态切换服务器)

 重点注意:

   实现client 的连接的方法不能通过Springboot  的@service @Autowired来实现  不然无法实现动态切换服务器  也就是Controller里面每次使用client的时候  都要new

        因为通过Bean实现的话,启动的时候就已经加载完成了,无法实现动态加载

 

声明:该文章参考公司同事(章帅)的文章

 

  

原文地址:https://www.cnblogs.com/emars/p/12750492.html