天天看點

SkyWalking agent 插件自動化測試實踐

前言

本文主要介紹SkyWalking agent 插件自動化測試架構組成及測試流程,以及一個實際的自動化測試testcase。

SkyWalking 插件自動化測試架構介紹

相關文檔:

[SkyWalking Plugin automatic test framework]

(

https://github.com/apache/skywalking/blob/master/docs/en/guides/Plugin-test.md)

這個自動化測試架構主要包含下面幾個部分:

  1. 測試環境docker鏡像
  2. 自動化測試腳本
  3. testcase工程
  4. 結果驗證工具

提供了兩種測試環境:JVM-container 和 Tomcat-container,可以在建立testcase工程時選擇使用的測試環境,官方推薦使用JVM-container。

其中JVM-container 可以了解為用于運作基于SpringBoot的testcase項目,包含啟動腳本,可以修改啟動的JVM參數,靈活性更好。

主要使用到兩個腳本:

  • 建立testcase工程

    ${SKYWALKING_HOME}/test/plugin/generator.sh

    執行腳本後,根據提示輸入testcase的類型,名稱等資訊,腳本自動建立一個可以編譯運作的樣例項目。
  • 運作測試案例

    ${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支援的版本清單,預設為空不會進行檢查,可以改為

all

表示全部
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)

  1. 啟用調試模式 (--debug),保留日志檔案目錄logs

    bash ./test/plugin/run.sh --debug mytest-scenario

  2. 從日志檔案提取收集的資料

    日志檔案目錄:

    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": [
        {
        ....
        }
      ]
    }
  ]
}           
  1. 将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'