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