前言
最早接觸“零侵入”一詞宁改,源于筆者參加美團(tuán)舉辦的測(cè)試技術(shù)沙龍活動(dòng)缕探。活動(dòng)上还蹲,去哪兒網(wǎng)的童鞋介紹其自主研發(fā)的接口自動(dòng)化測(cè)試框架Qunit時(shí)爹耗,提到了一項(xiàng)關(guān)鍵技術(shù):零侵入切面技術(shù),該技術(shù)方案最大優(yōu)點(diǎn)是:無(wú)需修改代碼實(shí)現(xiàn)mock功能谜喊,舉例說(shuō)明如下潭兽。
假如被測(cè)接口里面調(diào)用了第三方接口,由于第三方接口的不確定性斗遏,對(duì)于某些測(cè)試場(chǎng)景(比如請(qǐng)求超時(shí)山卦、特定錯(cuò)誤碼測(cè)試等),測(cè)試人員往往需要開(kāi)發(fā)人員添加mock來(lái)配合測(cè)試诵次,這種工作效率相對(duì)來(lái)說(shuō)是比較低的账蓉,而且也不利于自動(dòng)化測(cè)試的開(kāi)展。
零侵入技術(shù)把mock主動(dòng)權(quán)交接給測(cè)試人員管理逾一,無(wú)需開(kāi)發(fā)再去修改代碼铸本、部署測(cè)試環(huán)境等一系列動(dòng)作。測(cè)試人員只需根據(jù)具體的測(cè)試場(chǎng)景編寫對(duì)應(yīng)三方接口的mock腳本遵堵,啟動(dòng)mock服務(wù)即可归敬。通過(guò)靈活編寫mock腳本,我們可以覆蓋各種特殊的測(cè)試場(chǎng)景鄙早。
比如需要在系統(tǒng)測(cè)試環(huán)境mock上圖的“第三方接口1”汪茧,讓其返回超時(shí)。測(cè)試人員只需編寫mock1腳本限番,啟動(dòng)mock服務(wù)舱污,請(qǐng)求“被測(cè)試接口”時(shí)即可觸發(fā)調(diào)用mock server,而非真實(shí)接口“第三方接口1”弥虐,整個(gè)過(guò)程并沒(méi)有修改被測(cè)接口任何代碼扩灯。
同理,如果想同時(shí)mock“第三方接口1”和“第三方接口2”霜瘪,只需再編寫一個(gè)mock2腳本珠插,以此類推。
零侵入實(shí)現(xiàn)原理
Java程序運(yùn)行時(shí)颖对,必須經(jīng)過(guò)編譯和運(yùn)行兩個(gè)步驟捻撑。首先將后綴名為.java的源文件進(jìn)行編譯,最終生成.class的字節(jié)碼文件,然后將字節(jié)碼文件加載到內(nèi)存進(jìn)行解析執(zhí)行顾患。零侵入技術(shù)要做的就是在.class文件被加載前番捂,對(duì)其進(jìn)行修改,以達(dá)到我們的目的江解。字節(jié)碼修改工具有ASM设预、Javassist等,接下來(lái)筆者將基于Java Agent+Javassist來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的零侵入mock測(cè)試場(chǎng)景犁河,對(duì)于更復(fù)雜的應(yīng)用場(chǎng)景鳖枕,有興趣的童鞋可深入專研。
Java Agent介紹
JavaAgent 是運(yùn)行在 main方法之前的攔截器桨螺,其內(nèi)定的方法名是premain宾符,也就是說(shuō)先執(zhí)行premain方法,然后再執(zhí)行main方法彭谁。通過(guò)增加premain方法,即可實(shí)現(xiàn)一個(gè)JavaAgent允扇。
Javassist介紹
Javassist是一個(gè)開(kāi)源的分析缠局、編輯和創(chuàng)建Java字節(jié)碼的類庫(kù)。關(guān)于java字節(jié)碼的處理考润,目前有很多工具狭园,如bcel,asm糊治。不過(guò)這些都需要直接跟虛擬機(jī)指令打交道唱矛。如果你不想了解虛擬機(jī)指令,可以采用javassist井辜。javassist是jboss的一個(gè)子項(xiàng)目绎谦,其主要的優(yōu)點(diǎn)在于簡(jiǎn)單,而且快速粥脚。直接使用java編碼的形式窃肠,而不需要了解虛擬機(jī)指令,就能動(dòng)態(tài)改變類的結(jié)構(gòu)刷允,或者動(dòng)態(tài)生成類冤留。
案例
發(fā)短信接口sendMsg調(diào)用了第三方接口toSendSmsBySingle,下面通過(guò)零侵入的方式實(shí)現(xiàn)第三方接口返回指定的響應(yīng)報(bào)文树灶。
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方法執(zhí)行========");
//System.out.println(agentOps);
// 添加Transformer
inst.addTransformer(new ClassFileTransformerImp());
}
}
編寫ClassFileTransformer的實(shí)現(xiàn)ClassFileTransformerImp天通,主要功能是使用javassist來(lái)修改字節(jié)碼文件泊窘,在第40行通過(guò)插入“url = http://localhost:8187/v1/toSendSmsBySingle;”來(lái)改變代碼中url的值,從而請(qǐng)求mockserver,其中l(wèi)ocalhost: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);
//設(shè)置class文件的位置谜洽,實(shí)際運(yùn)用時(shí)應(yīng)替換為相對(duì)路徑
cPool.insertClassPath("D:\\gittest_pro\\iiAccount\\iiAccount-adapter\\target\\classes");
//獲取該class對(duì)象
CtClass cClass = cPool.get("com.bank.iiacc.adapter.MsgServiceAdapter");
//獲取到對(duì)應(yīng)的方法
CtMethod cMethod = cClass.getDeclaredMethod("sendMsg");
//通過(guò)insertAt可引用局部變量。
cMethod.insertAt(40, "{url = \"http://localhost:8187/v1/toSendSmsBySingle\";}");
//替換原有的文件吴叶,實(shí)際運(yùn)用時(shí)應(yīng)替換為相對(duì)路徑
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打包
常見(jiàn)的打包技術(shù)參考idea打包jar的多種方式,以下介紹其中一種方式蚌卤。
-
第1步
file-project structure -
第2步
add jar -
第3步实束,修改路徑。
修改為resource目錄 - 第4步
修改resources目錄下的MANIFEST.MF文件逊彭,增加第2咸灿、3行內(nèi)容。
Manifest-Version: 1.0
Premain-Class: MyAgent //增加第1點(diǎn)的MyAgent類路徑
Can-Redefine-Classes: true //增加
Class-Path: javassist-3.20.0-GA.jar
Main-Class:
-
第5步侮叮,點(diǎn)擊ok避矢。
導(dǎo)出jar包 -
第6步
build-build artifacts
build -
第7步,build完成后囊榜,out目錄下已導(dǎo)出了對(duì)應(yīng)的jar包
javaAgent.jar
除了上述的打包方式审胸,亦可通過(guò)pom配置自動(dòng)打包。
<?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>com.demo.javassist</groupId>
<artifactId>javassist</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<springframework.version>4.3.8.RELEASE</springframework.version>
</properties>
<dependencies>
<dependency>
<groupId>org.javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.20.0-GA</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${springframework.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<archive>
<manifestEntries>
<Premain-Class>MyAgent</Premain-Class>
</manifestEntries>
</archive>
<appendAssemblyId>false</appendAssemblyId>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
雙擊package后卸勺,target目錄下生成打包文件砂沛。
3、配置tomcat啟動(dòng)參數(shù)
- 增加以下啟動(dòng)參數(shù)曙求。
-javaagent:D:\gittest_pro\javaAgent\out\artifacts\javaAgent_jar\javaAgent.jar
-
啟動(dòng)tomcat
啟動(dòng)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\":\"業(yè)務(wù)處理失敗\",\"status\":\"GW-10510\",\"sign\":\"6tbbBajxsMTsql1Gl/VSsI7BHilAvCtA9J0FGiN7+p3Nde7vwZVd9taneNIp4M1zsRhqXXHMFTp67ZFTUItcI8PB4UFnltXomCCW1Jya7dI+hpQilUs2rLQ1WcumGN3GqjWaE472FQbOX2muzcUjJbsMosTo+P0SPawhO5m83Uw=\"}",
"_mode": "text",
"_proxyResponseTime": 135
}
}
]
}
5马绝、啟動(dòng)mock服務(wù)
啟動(dòng)moutebank。
mb --configfile d:\mountebank_ejs\main.ejs --allowInjection
6挣菲、接口請(qǐng)求
發(fā)送接口請(qǐng)求
查看MsgServiceAdapter.class文件富稻,可發(fā)現(xiàn)java agent確實(shí)發(fā)揮了作用,url被重新賦值白胀。
查看控制臺(tái)日志椭赋,可發(fā)現(xiàn)請(qǐng)求第三方接口toSendSmsBySingle時(shí),確實(shí)返回了mock的響應(yīng)報(bào)文或杠,并沒(méi)有去請(qǐng)求真實(shí)的第三方接口哪怔。
總結(jié)
無(wú)論是手工測(cè)試,還是自動(dòng)化測(cè)試,零侵入mock技術(shù)無(wú)疑都有大量的應(yīng)用場(chǎng)景认境,但要用好這門技術(shù)卻不是一件容易的事胚委,任何技術(shù)的應(yīng)用都是一個(gè)循序漸進(jìn)、挖坑填坑的過(guò)程叉信,筆者也在專研中亩冬。
相關(guān)學(xué)習(xí)資料
去哪兒自動(dòng)化測(cè)試框架Qunit中的零侵入切面技術(shù)應(yīng)用及分布式運(yùn)行平臺(tái)
深入理解JVM之Java字節(jié)碼(.class)文件詳解
Javassist 操作手冊(cè)
Javassist 使用指南(一)
Javassist 使用指南(二)
Javassist 使用指南(三)
Java動(dòng)態(tài)編程之javassist
JAVA AOP編程之:Javassist