AREX-攜程無程式碼侵入的自動化迴歸測試平臺

語言: CN / TW / HK

背景

對於一個初上線的簡單服務,只需通過常規的自動化測試加上人工即可解決,但我們線上核心的業務系統往往比較複雜,通常也會頻繁的需求迭代,如何保證被修改後的系統原有業務的正確性就比較重要。常規的自動化測試需要投入大量的人力資源,準備測試資料、指令碼等,並且覆蓋率通常也不高,難以滿足要求。

為了保證一個線上系統的穩定性,開發和測試人員都面臨不少的挑戰:

  • 開發完成後難以快速本地驗證,發現初步的問題,容易陷入提測 → 發現 bug → fix → 提測的迴圈
  • 準備測試資料、自動化指令碼編寫和維護需要大量的人力成本,而且難以保證覆蓋率
  • 寫服務難於驗證,而且測試會產生髒資料,例如我們的核心交易系統,可能會往資料庫、訊息佇列、Redis 等寫入資料,這部分資料往往比較難以驗證,測試產生的資料也難於清理
  • 線上問題難以本地復現,排查困難

AREX 介紹

AREX 通過複製線上真實流量到測試環境進行自動化迴歸測試,解決迴歸測試的難題。

AREX 採用 java 的 instrument 實現了無程式碼侵入的資料採集和自動化 Mock,智慧的 Mock 機制使測試執行程式碼集中在待測應用,不會產生真正的外部互動(DB 的寫入、其它服務的呼叫),也完美支援了寫介面的測試(如核心交易系統、庫存系統等)。

原理示例如下:

我們假定生產環境應用會正常的響應使用者的請求,通過 aop 的方式將請求入參及返回結果以及執行過程中的一些快照資料例如訪問資料庫的入參和返回結果、訪問遠端伺服器的入參及結果儲存下來。然後將快照資料傳送給測試機器(程式碼發生變化的機器)完成一次回放過程。通過將落庫資料、呼叫後臺請求的資料以及返回結果和線上真實請求發生時的資料進行對比,發現其中的差異,從而識別被測試系統的問題。

  • xxxTestCase:採集下來的資料在回放時作為測試 CASE
  • xxxMock:在回放時會使用採集的資料進行 Mock,代替真正的資料訪問
  • xxxExpect 和 xxxReal:在測試結束後會驗證對應的資料,發現程式碼中潛藏的隱患

技術原理:

在 JDK1.5 中,Java 引入了 java.lang.Instrument 包,該包提供了一些工具幫助開發人員在 Java 程式執行時,動態修改系統中的 Class,以此實現對原類的功能增強。現在有很多工具都是基於此技術實現的,例如阿里開源的 arthas、監控工具 SkyWalking 等,AREX 的資料採集和自動 Mock 也是基於此技術實現。

平臺優勢

  1. 低成本:
  • 無程式碼侵入,基本無接入成本
  • 無需編寫測試用例,海量的線上請求也能保證高覆蓋率
  • 插樁程式碼足夠簡單,效能損耗低
  1. 支援寫驗證:支援資料庫、訊息佇列、Redis資料的驗證,甚至支援驗證執行時的記憶體資料,並且測試時不會產生髒資料。

  2. 測試 CASE 高穩定:支援各種主流技術框架的自動資料採集和 Mock,參見:arex_java,並且支援了本地時間、快取,在回放時精準還原生產執行時的資料環境。

  3. 快速復現線上問題:支援一鍵本地除錯,可以快速本地除錯線上問題

  4. 安全穩定,程式碼隔離,也實現了健康管理,在系統繁忙時會智慧降低或關閉資料採集頻率

  5. 良好的功能測試支援,支援測試指令碼,也可對採集的資料進行簡單的編輯實現固定測試觀點的測試,避免大量的測試資料準備

技術實現

我們採用了 ByteBuddy 庫實現的位元組碼修改,在實現過程中也遇到了各式的挑戰。

Trace 傳遞

AREX 在進行資料採集時,同一個請求,會採集下來多條資料(Request/Response、其它服務呼叫的請求響應等),我們需要把這些資料串聯起來,這樣才能完整地作為一個測試用例。而我們的應用往往採用了非同步框架,也大量用到了多執行緒等,這給資料的串聯帶來很大的困難。

1. Java Executors

Java 和各種框架裡提供了眾多的執行緒池實現,我們要保證 Trace 資料能正確的跨執行緒傳遞,首先我們修飾了 Runnable/Callable, 如下:

public class RunnableWrapper implements Runnable {
    private final Runnable runnable;
    private final TraceTransmitter traceTransmitter;

    private RunnableWrapper(Runnable runnable) {
        this.runnable = runnable;
        this.traceTransmitter = TraceTransmitter.create();
    }

    @Override
    public void run() {
        try (TraceTransmitter tm = traceTransmitter.transmit()) {
            runnable.run();
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        RunnableWrapper that = (RunnableWrapper) o;
        return runnable.equals(that.runnable);
    }

    @Override
    public int hashCode() {
        return runnable.hashCode();
    }

    @Override
    public String toString() {
        return this.getClass().getName() + " - " + runnable.toString();
    }

    public static Runnable get(Runnable runnable) {
        if (null == runnable  || TraceContextManager.get() == null) {
            return runnable;
        }

        if (runnable instanceof RunnableWrapper) {
            return runnable;
        }
        return new RunnableWrapper(runnable);
    }
}

然後程式碼修飾各種執行緒池,把 Runnable/Callable 替換掉,而 Wrapper 內部通過 TraceTransmitter 保證 Trace 的正確傳遞。

2. ForkJoinPool

CompletableFuture、資料集的並行 stream 處理預設使用的是 ForkJoinPool,重平行計算的應用也經常採用,這個和常規的執行緒池實現有較大的區別,需要單獨處理,我們對 ForkJoinPool 的任務單元 ForkJoinTask 進行了修飾,這個類實現比較複雜,難於像 Runnable 那樣簡單處理,而且為了不破壞原有的類結構(Agent on attach 方式也不支援修改),沒有在這個類上新增欄位實現資料中轉,而是採用了一個 WeakCache 做資料緩衝,可以保證任務生成和執行執行緒之間的 Trace 傳遞。

3. 非同步

Java 生態中存在很多非同步框架(Reactor、RxJava etc),也有很多類庫提供了非同步實現,如 lettuce 就提供了同/非同步訪問 Redis 的方式。不同的場景往往需要不同的解決方法。以 Apache AsyncClient 為例,是以固定執行的執行緒監聽響應,併發起 Callback,我們要保證呼叫、監聽、回撥整個流程中多個跨執行緒的 Trace 傳遞,具體實現可以參見 Apache AsyncClient

版本管理

流行的元件往往存在多個版本同時在不同的系統中使用,不同的版本實現方式差別可能很大,甚至不相容,AREX 中也有提供多個版本的支援(如 Jedis),我們就要保證能按正確的版本進行位元組碼注入,避免執行錯誤。位元組碼注入是在類載入時進行的,這樣我們就必須在這些類載入前識別出應用依賴的元件版本,從而在類載入時進行版本的匹配,保證正確的程式碼注入。

時間管理

很多業務系統是時間敏感的,不同的時間訪問往往會返回不同的結果,因此我們也實現了時間的 Mock 功能。由於回放是並行執行的,修改測試機器的機器時間是不合適的(而且很多伺服器也不能修改當前時間),所以還是在程式碼層面上實現的時間的 Mock。在資料採集時,我們針對每個用例記錄了當前時間,在回放時,會對 System.currentTimeMills(Java 的很多時間底層都是通過這個方法實現)方法進行代理,然後計算多次訪問的時間差(System.nanoTime 實現)來保證時間的準確性, 不過仍然可能存在毫秒級的差距。

本地快取

業務應用中可能使用了各式的快取來提升執行時的效能,不同的環境這些資料可能存在較大的差異(多個環境的資料同步往往是一個比較複雜的過程),這些資料差異可能會導致完全不同的執行結果,為了避免這個問題,AREX 也支援了本地快取資料的採集和 Mock 功能,只需要進行一些簡單的配置,即可實現資料的自動採集和 Mock,當然這個功能也可支援各種記憶體資料的 Mock 功能。

程式碼隔離、互通

為了系統的穩定性,AREX agent 的框架程式碼是在一個獨立的 Class loader 中載入,和應用程式碼並不互通,為了保證注入的程式碼可以正確在執行時被訪問,我們也對 ClassLoader 進行了簡單的修飾,保證執行時的程式碼會被正確的 ClassLoader 載入(想想 SpringBoot 的 LaunchedURLClassLoader)。

專案現狀

AREX 專案在攜程機票內部發起,經過一年多的發展,逐漸推廣到酒店、旅遊、商旅、平臺等多個部門,而且攜程內部的多個團隊已經用 AREX 代替了其它自動化工具和手工測試來進行迴歸測試。目前我們已將專案開源:AREX。鑑於各種技術框架、類庫茫茫多,支援的必然還不太夠,歡迎各位有志之士共同來完成,讓我們共同構造一個簡單、高效的研發測試方式(試想針對每次迭代,程式碼提交後測試自動執行,並反饋測試報告,開發和測試人員只需要關注在新業務的研發、驗證上即可,脫離那些繁瑣的資料和指令碼,以及測試了無數遍的舊功能)。

加入官方QQ交流群:656108079