前言
最早接觸“零侵入”一詞,源於筆者參加美團舉辦的測試技術沙龍活動。活動上,去哪兒網的童鞋介紹其自主研發的接口自動化測試框架Qunit時,提到了一項關鍵技術:零侵入切面技術,該技術方案最大優點是:無需修改代碼實現mock功能,舉例說明如下。
假如被測接口裏面調用了第三方接口,由於第三方接口的不確定性,對於某些測試場景(比如請求超時、特定錯誤碼測試等),測試人員往往需要開發人員添加mock來配合測試,這種工作效率相對來說是比較低的,而且也不利於自動化測試的開展。
零侵入技術把mock主動權交接給測試人員管理,無需開發再去修改代碼、部署測試環境等一系列動作。測試人員只需根據具體的測試場景編寫對應三方接口的mock腳本,啓動mock服務即可。通過靈活編寫mock腳本,我們可以覆蓋各種特殊的測試場景。
比如需要在系統測試環境mock上圖的“第三方接口1”,讓其返回超時。測試人員只需編寫mock1腳本,啓動mock服務,請求“被測試接口”時即可觸發調用mock server,而非真實接口“第三方接口1”,整個過程並沒有修改被測接口任何代碼。
同理,如果想同時mock“第三方接口1”和“第三方接口2”,只需再編寫一個mock2腳本,以此類推。
零侵入實現原理
Java程序運行時,必須經過編譯和運行兩個步驟。首先將後綴名爲.java的源文件進行編譯,最終生成.class的字節碼文件,然後將字節碼文件加載到內存進行解析執行。零侵入技術要做的就是在.class文件被加載前,對其進行修改,以達到我們的目的。字節碼修改工具有ASM、Javassist等,接下來筆者將基於Java Agent+Javassist來實現一個簡單的零侵入mock測試場景,對於更復雜的應用場景,有興趣的童鞋可深入專研。
Java Agent介紹
JavaAgent 是運行在 main方法之前的攔截器,其內定的方法名是premain,也就是說先執行premain方法,然後再執行main方法。通過增加premain方法,即可實現一個JavaAgent。
Javassist介紹
Javassist是一個開源的分析、編輯和創建Java字節碼的類庫。關於java字節碼的處理,目前有很多工具,如bcel,asm。不過這些都需要直接跟虛擬機指令打交道。如果你不想了解虛擬機指令,可以採用javassist。javassist是jboss的一個子項目,其主要的優點在於簡單,而且快速。直接使用java編碼的形式,而不需要了解虛擬機指令,就能動態改變類的結構,或者動態生成類。
案例
發短信接口sendMsg調用了第三方接口toSendSmsBySingle,下面通過零侵入的方式實現第三方接口返回指定的響應報文。
1、編寫agent
pom.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>JavaAgent</groupId>
<artifactId>javaAgent</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
</dependencies>
</project>
編寫premain方法邏輯。
import java.lang.instrument.Instrumentation;
public class MyAgent {
public static void premain(String agentOps, Instrumentation inst) {
System.out.println("=========premain方法執行========");
//System.out.println(agentOps);
// 添加Transformer
inst.addTransformer(new ClassFileTransformerImp());
}
}
編寫ClassFileTransformer的實現ClassFileTransformerImp,主要功能是使用javassist來修改字節碼文件,在第40行通過插入“url = http://localhost:8187/v1/toSendSmsBySingle;”來改變代碼中url的值,從而請求mockserver,其中localhost:8187爲下文提到的mockserver地址。
import javassist.*;
import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class ClassFileTransformerImp implements ClassFileTransformer {
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (className.equals("com.bank.iiacc.adapter.MsgServiceAdapter")) {
try {
System.out.println("類名:" + className);
ClassPool cPool = new ClassPool(true);
//設置class文件的位置,實際運用時應替換爲相對路徑
cPool.insertClassPath("D:\\gittest_pro\\iiAccount\\iiAccount-adapter\\target\\classes");
//獲取該class對象
CtClass cClass = cPool.get("com.bank.iiacc.adapter.MsgServiceAdapter");
//獲取到對應的方法
CtMethod cMethod = cClass.getDeclaredMethod("sendMsg");
//通過insertAt可引用局部變量。
cMethod.insertAt(40, "{url = \"http://localhost:8187/v1/toSendSmsBySingle\";}");
//替換原有的文件,實際運用時應替換爲相對路徑
cClass.writeFile("D:\\gittest_pro\\iiAccount\\iiAccount-adapter\\target\\classes");
System.out.println("=======修改完成=========");
} catch (NotFoundException e) {
e.printStackTrace();
} catch (CannotCompileException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
return null;
}
}
2、agent打包
常見的打包技術參考idea打包jar的多種方式,以下介紹其中一種方式。
-
第1步
-
第2步
-
第3步,修改路徑。
- 第4步
修改resources目錄下的MANIFEST.MF文件,增加第2、3行內容。
Manifest-Version: 1.0
Premain-Class: MyAgent //增加第1點的MyAgent類路徑
Can-Redefine-Classes: true //增加
Class-Path: javassist-3.20.0-GA.jar
Main-Class:
-
第5步,點擊ok。
-
第6步
-
第7步,build完成後,out目錄下已導出了對應的jar包
3、配置tomcat啓動參數
- 增加以下啓動參數。
-javaagent:D:\gittest_pro\javaAgent\out\artifacts\javaAgent_jar\javaAgent.jar
-
啓動tomcat
4、編寫mock腳本
以moutebank舉例,詳情參考筆者另外一篇文章《Mock service之Mountebank入門》。
- main.ejs腳本如下。
{
"imposters": [
<% include proxy.ejs %>,
<% include iiacct.ejs %>
]
}
- iiacc.ejs腳本如下。
{
"port": 8187,
"protocol": "http",
"stubs": [
<% include toSendSmsBySingle.ejs %>
]
}
- toSendSmsBySingle.ejs腳本如下。
{
"predicates": [
{
"contains": {
"path": "/v1/toSendSmsBySingle"
}
}
],
"responses": [
{
"is": {
"statusCode": 500,
"headers": {
"Server": "Apache-Coyote/1.1",
"Content-Type": "text/json;charset=UTF-8",
"Content-Length": 298,
"Date": "Tue, 05 Sep 2017 06:49:14 GMT",
"Connection": "close"
},
"body": "{\"data\":{\"errCode\":\"iia-trade-00010\",\"errMsg\":\"商戶不存在8888\"},\"message\":\"業務處理失敗\",\"status\":\"GW-10510\",\"sign\":\"6tbbBajxsMTsql1Gl/VSsI7BHilAvCtA9J0FGiN7+p3Nde7vwZVd9taneNIp4M1zsRhqXXHMFTp67ZFTUItcI8PB4UFnltXomCCW1Jya7dI+hpQilUs2rLQ1WcumGN3GqjWaE472FQbOX2muzcUjJbsMosTo+P0SPawhO5m83Uw=\"}",
"_mode": "text",
"_proxyResponseTime": 135
}
}
]
}
5、啓動mock服務
啓動moutebank。
mb --configfile d:\mountebank_ejs\main.ejs --allowInjection
6、接口請求
發送接口請求
查看MsgServiceAdapter.class文件,可發現java agent確實發揮了作用,url被重新賦值。
查看控制檯日誌,可發現請求第三方接口toSendSmsBySingle時,確實返回了mock的響應報文,並沒有去請求真實的第三方接口。
總結
無論是手工測試,還是自動化測試,零侵入mock技術無疑都有大量的應用場景,但要用好這門技術卻不是一件容易的事,任何技術的應用都是一個循序漸進、挖坑填坑的過程,筆者也在專研中。
相關學習資料
去哪兒自動化測試框架Qunit中的零侵入切面技術應用及分佈式運行平臺
深入理解JVM之Java字節碼(.class)文件詳解
Javassist 操作手冊
Javassist 使用指南(一)
Javassist 使用指南(二)
Javassist 使用指南(三)
Java動態編程之javassist
JAVA AOP編程之:Javassist