自動化測試如何訪問不同的環境對應服務例項
首先介紹做一下場景介紹:
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實現的話,啟動的時候就已經載入完成了,無法實現動態載入
宣告:該文章參考公司同事(章帥)的文章