前言
本文主要介紹SkyWalking agent 插件自動化測試架構組成及測試流程,以及一個實際的自動化測試testcase。
SkyWalking 插件自動化測試架構介紹
相關文檔:
[SkyWalking Plugin automatic test framework]
(
https://github.com/apache/skywalking/blob/master/docs/en/guides/Plugin-test.md)
這個自動化測試架構主要包含下面幾個部分:
- 測試環境docker鏡像
- 自動化測試腳本
- testcase工程
- 結果驗證工具
提供了兩種測試環境:JVM-container 和 Tomcat-container,可以在建立testcase工程時選擇使用的測試環境,官方推薦使用JVM-container。
其中JVM-container 可以了解為用于運作基于SpringBoot的testcase項目,包含啟動腳本,可以修改啟動的JVM參數,靈活性更好。
主要使用到兩個腳本:
- 建立testcase工程
執行腳本後,根據提示輸入testcase的類型,名稱等資訊,腳本自動建立一個可以編譯運作的樣例項目。${SKYWALKING_HOME}/test/plugin/generator.sh
- 運作測試案例
${SKYWALKING_HOME}/test/plugin/run.sh ${scenario_name}
${SKYWALKING_HOME}/test/plugin/run.sh -f ${scenario_name}
參數說明:${SKYWALKING_HOME}/test/plugin/run.sh --debug ${scenario_name}
- -f 參數強制重新建立鏡像,在修改SkyWalking agent或plugin後需要添加-f參數,否則不能更新測試鏡像中的agent程式。隻改動testcase時不需要-f參數,減少啟動時間。
- --debug 啟用調試模式,推薦使用此參數,可以保留測試過程的logs。
這裡隻介紹JVM-container類型的工程,實際上為基于SpringBoot的testcase應用。
[plugin-scenario]
|- [bin]
|- startup.sh
|- [config]
|- expectedData.yaml
|- [src]
|- [main]
|- ...
|- [resource]
|- log4j2.xml
|- pom.xml
|- configuration.yaml
|- support-version.list
[] = directory
工程檔案說明:
檔案/目錄 | 說明 |
---|---|
bin/startup.sh | testcase 應用啟動腳本 |
config/expectedData.yaml | 測試結果驗證資料 |
configuration.yaml | testcase 配置,包含類型、啟動腳本、檢測url等 |
support-version.list | testcase支援的版本清單,預設為空不會進行檢查,可以改為 表示全部 |
pom.xml | maven 項目描述檔案 |
[src] | testcase 源碼目錄 |
其中對新手來說最難的是編寫測試結果驗證資料
expectedData.yaml
,資料格式不是很複雜,但要手寫出來還是比較困難的。後面會提及一些技巧,可以從日志檔案
logs/validatolr.out
中提取驗證資料。
測試結果驗證工具
SkyWalking 自動化測試工具的精髓所做應該就是自動驗證測試結果資料,支援多種比對條件表達式,可以靈活處理一些動态變化的資料。其中關鍵的是skywalking-validator-tools.jar工具,其源碼repo為
skywalking-agent-test-tool。
validator的代碼量不大,通過閱讀代碼,可以了解
expectedData.yaml
的驗證過程,了解驗證資料的格式。
自動化測試流程
bash ./test/plugin/run.sh --debug xxxx-scenario
-> 準備測試的workspace
-> 編譯testcase工程
-> 啟動plugin-runner-helper 生成docker啟動腳本等
-> scenario.sh
-> 啟動測試環境docker執行個體
-> docker容器中執行 /run.sh
-> collector-startup.sh
-> 啟動skywalking-mock-collector(測試資料收集服務)
-> testcase/bin/startup.sh
-> 啟動testcase應用(-javaagent加載skywalking-agent.jar)
-> 循環healthCheck,等待testcase應用啟動完畢
-> 通路entryService url,觸發測試用例
-> 接收測試資料,寫入到data/actualData.yaml檔案
-> 啟動skywalking-validator-tools.jar驗證測試結果資料
-> 結束
設計測試用例
測試結果驗證工具隻能收集testcase應用的APM資料,比如span和logEvent等,不能收集http請求的傳回内容,但可以收集到請求的狀态碼。
測試用例互動過程
這裡僅介紹通過http請求互動,收集http相關資料,其它的資料與具體插件相關。比如測試redis apm插件時,可以收集到redis事件,包含執行的redis指令語句。
通過
test/plugin/generator.sh
指令生成測試用例中包含兩個url,一個是healthCheck,一個是entryService。
1)healthCheck一般不需要管,用于探測testcase應用是否啟動成功。如果編寫的testcase有需要初始化的資料,請在healthCheck傳回成功之前進行處理。
2)entryService是測試的入口url,healthCheck通過後,會接着通路entryService。可以在entryService的方法中進行調用測試方法,失敗時傳回4xx/5xx狀态碼。
這裡要注意一個問題:
org.apache.skywalking.apm.testcase.*
包下面的類不會被SkyWalking agent增強,這意味着這個包裡面所有的類都不會被插件增強處理,比如标注了@Controller、@Component等的類并不會被apm-spring-annotation-plugin-*.jar 插件增強。如果要測試類增強的相關代碼在testcase中,則要将代碼放到這個包裡面
test.org.apache.skywalking.apm.testcase.*
如何通過收集的APM資料判斷測試成功或者失敗?
1)http處理成功傳回200/3xx時收集到span資訊沒有status_code,處理異常傳回4xx/5xx錯誤時會産生一個tag記錄status_code,可以用于驗證區分測試結果。參考代碼如下:
@RequestMapping("/dosomething")
public ResponseEntity dosomething() {
// check testcase is successful or not
if (isTestSuccess()) {
return ResponseEntity.ok("success");
} else {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("failure");
}
}
2)還可以通過抛出異常來産生status_code和logEvent
@RequestMapping("/xxx-scenario")
@ResponseBody
public String testcase() throws HttpStatusCodeException {
if (isTestSuccess()) {
return "success";
}
throw new RuntimeException("failure");
}
編寫測試結果驗證資料(expectedData.yaml)
- 啟用調試模式 (--debug),保留日志檔案目錄logs
bash ./test/plugin/run.sh --debug mytest-scenario
-
從日志檔案提取收集的資料
日志檔案目錄:
skywalking/test/plugin/workspace/mytest-scenario/all/logs
收集的資料可以從日志檔案
validatolr.out
從提取到,找到後面的actual data:
[2020-06-10 07:31:56:674] [INFO] - org.apache.skywalking.plugin.test.agent.tool.validator.assertor.DataAssert.assertEquals(DataAssert.java:29) - actual data:
{
"segmentItems": [
{
"serviceName": "mytest-scenario",
"segmentSize": "2",
"segments": [
{
....
}
]
}
]
}
-
将actual data的json資料轉換為yaml
打開其他測試場景的expectedData.yaml,如
的第一段内容:httpclient-3.x-scenario/config/expectedData.yaml
segmentItems:
- serviceName: httpclient-3.x-scenario
segmentSize: ge 3
segments:
- segmentId: not null
spans:
- operationName: /httpclient-3.x-scenario/case/context-propagate
operationId: 0
parentSpanId: -1
spanId: 0
spanLayer: Http
startTime: nq 0
endTime: nq 0
componentId: 1
isError: false
spanType: Entry
peer: ''
tags:
- {key: url, value: 'http://localhost:8080/httpclient-3.x-scenario/case/context-propagate'}
- {key: http.method, value: GET}
refs:
- {parentEndpoint: /httpclient-3.x-scenario/case/httpclient, networkAddress: 'localhost:8080',
refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not null, parentServiceInstance: not
null, parentService: httpclient-3.x-scenario, traceId: not null}
skipAnalysis: 'false'
expectedData.yaml是用于檢查測試結果的比對模闆,隻有簡單的幾種比對表達式如下表:
Operator for number
| Operator | Description |
| :--- | :--- |
|
nq
| Not equal |
eq
| Equal(default) |
ge
| Greater than or equal |
gt
| Greater than |
Operator for String
not null
| Not null |
null
| Null or empty String |
eq
| Equal(default) |
比如segmentId是随機生成的,那麼可以寫成
segmentId: not null
這樣就可以比對任意字元串。開始時間是變化的可以寫成
startTime: nq 0
,隻判斷其是否大于0就可以。
對照擷取到的actual data json,修改對應的字段就可以了。可以忽略檢查healthCheck的資料,隻需要寫上關鍵的segment。看一個案例,actual data 如下:
{
"segmentItems": [
{
"serviceName": "mytest-scenario",
"segmentSize": "2",
"segments": [
{
... healthCheck ...
},
{
"segmentId": "ab32f6a2774347958318b0fb06ccd2f0.33.15917743102950000",
"spans": [
{
"operationName": "/case/mytest-scenario",
"operationId": "0",
"parentSpanId": "-1",
"spanId": "0",
"spanLayer": "Http",
"tags": [
{
"key": "url",
"value": "http://localhost:8080/case/mytest-scenario"
},
{
"key": "http.method",
"value": "GET"
}
],
"startTime": "1591774310295",
"endTime": "1591774310316",
"componentId": "14",
"spanType": "Entry",
"peer": "",
"skipAnalysis": "false"
}
]
}
]
}
]
}
對應的expectedData.yaml(忽略檢查healthCheck的資料):
segmentItems:
- serviceName: mytest-scenario
segmentSize: ge 1
segments:
- segmentId: not null
spans:
- operationName: /case/mytest-scenario
operationId: 0
parentSpanId: -1
spanId: 0
spanLayer: Http
startTime: nq 0
endTime: nq 0
componentId: ge 1
isError: false
spanType: Entry
peer: ''
tags:
- {key: url, value: 'http://localhost:8080/case/mytest-scenario'}
- {key: http.method, value: GET}
skipAnalysis: 'false'
常見錯誤處理
-
docker 容器執行個體名沖突
./test/plugin/run.sh 出現下面的錯誤:
docker: Error response from daemon: Conflict. The container name "/xxxx-scenario-all-local" is already in use by container "42cdee17e557bb71...". You have to remove (or rename) that container to be able to reuse that name.
解決辦法:
删除上次測試失敗留下來的容器執行個體:
docker rm xxxx-scenario-all-local
編寫自動化測試testcase
1. 生成testcase工程
> cd skywalking
> bash ./test/plugin/generator.sh
Sets the scenario name
>: mytest-scenario
Chooses a type of container, 'jvm' or 'tomcat', which is 'jvm-container' or 'tomcat-container'
>: jvm
Gives an artifactId for your project (default: mytest-scenario)
>:
Sets the entry name of scenario (default: mytest-scenario)
>:
scenario_home: mytest-scenario
type: jvm
artifactId: mytest-scenario
scenario_case: mytest-scenario
Please confirm: [Y/N]
>: y
[INFO] Scanning for projects...
2. 修改配置檔案
修改mytest-scenario/support-version.list,添加支援的版本,這裡用全部版本
all
。注意,預設沒有指定版本,不會啟動測試場景。
# lists your version here
all
3. 編寫測試用例
@RestController
@RequestMapping("/case")
public class CaseController {
private static final String SUCCESS = "Success";
@RequestMapping("/mytest-scenario")
@ResponseBody
public ResponseEntity testcase() {
//這裡簡單模拟,随機傳回成功或者失敗
SecureRandom random = new SecureRandom();
if (random.nextBoolean()) {
return ResponseEntity.ok(SUCCESS);
} else {
return ResponseEntity.notFound().build();
}
}
@RequestMapping("/healthCheck")
@ResponseBody
public String healthCheck() {
// your codes
return SUCCESS;
}
}
4. 本地測試testcase
bash ./test/plugin/run.sh --debug mytest-scenario
5. 提取測試收集的資料
從日志檔案提取收集的資料,actual data部分。
日志檔案:
skywalking/test/plugin/workspace/mytest-scenario/all/logs/validatolr.out
[2020-06-10 09:00:03:655] [INFO] - org.apache.skywalking.plugin.test.agent.tool.validator.assertor.DataAssert.assertEquals(DataAssert.java:29) - actual data:
{
"segmentItems": [
{
"serviceName": "mytest-scenario",
"segmentSize": "2",
"segments": [
{
"segmentId": "bfddda9bb70f49c694a90924b258a6da.32.15917795967760000",
"spans": [
{
"operationName": "/mytest-scenario/case/healthCheck",
"operationId": "0",
"parentSpanId": "-1",
"spanId": "0",
"spanLayer": "Http",
"tags": [
{
"key": "url",
"value": "http://localhost:8080/mytest-scenario/case/healthCheck"
},
{
"key": "http.method",
"value": "HEAD"
}
],
"startTime": "1591779596801",
"endTime": "1591779597069",
"componentId": "1",
"spanType": "Entry",
"peer": "",
"skipAnalysis": "false"
}
]
},
{
"segmentId": "bfddda9bb70f49c694a90924b258a6da.33.15917795971310000",
"spans": [
{
"operationName": "/mytest-scenario/case/mytest-scenario",
"operationId": "0",
"parentSpanId": "-1",
"spanId": "0",
"spanLayer": "Http",
"tags": [
{
"key": "url",
"value": "http://localhost:8080/mytest-scenario/case/mytest-scenario"
},
{
"key": "http.method",
"value": "GET"
}
],
"startTime": "1591779597132",
"endTime": "1591779597141",
"componentId": "1",
"spanType": "Entry",
"peer": "",
"skipAnalysis": "false"
}
]
}
]
}
]
}
6. 編寫expectedData.yaml
segmentItems:
- serviceName: mytest-scenario
segmentSize: ge 1
segments:
- segmentId: not null
spans:
- operationName: /case/mytest-scenario
operationId: 0
parentSpanId: -1
spanId: 0
spanLayer: Http
startTime: nq 0
endTime: nq 0
componentId: ge 1
isError: false
spanType: Entry
peer: ''
tags:
- {key: url, value: 'http://localhost:8080/case/mytest-scenario'}
- {key: http.method, value: GET}
skipAnalysis: 'false'