精準測試:基於 asm+javaparser 呼叫鏈差異化對比實踐

tlhymm42發表於2020-05-29

適用人群

如果你也面臨這些問題

  1. 接觸到的測試都是比較偏向底層,中臺化的服務,對上層業務會比較陌生

  2. 日常開發提交測試點時會出現遺漏的情況,導致測試階段漏測

  3. 開發測試比高,經常多個開發對一個測試,且日常發版頻繁

  4. 想自己搞一套精準測試框架輔助測試

那麼你可能也需要這麼一套精準測試思路,幫助你精準且快速的進行日常測試

依賴技能樹

在早幾年前就瞭解到可以透過一些抽象語法解析工具或框架,針對 java 專案做鏈路梳理,再透過鏈路逆向反推測試迴歸點,趁著這個機會,較為深入的梳理了一下相關的知識體系

  1. 掌握:java 程式設計,瞭解 jvm 大致原理,特別編譯階段,類載入階段

  2. 熟悉:asm,可將 class 檔案梳理為一條完整的呼叫鏈

  3. 熟悉:javaparser,可將 java 檔案解析為抽象語法樹(AST)

對標前幾年大肆推的 jacoco 用於精準測試,透過 AST 解析有以下優勢

  1. jacoco 只告訴你執行過程中程式碼的哪些行沒覆蓋,具體這行有什麼意義,為什麼要覆蓋,怎麼去覆蓋,這些你都無從得知;而透過呼叫鏈差異對比可更為精準的推送需要回歸的業務
  2. jacoco 大多隻能適用於單元測試,如果想整合測試使用還需要依賴 agent 注入;而透過呼叫鏈差異對比可透過接入 jenkins 在提測前就輸出測試點,不需要改動業務程式碼
  3. 相比 jacoco 拿來就可以用,需要了解更多的 jvm 基礎知識,同時擴充了個人知識體系

實踐

  • 呼叫鏈掃描
  1. 加強 Class/Method 事件篩選器,儲存父子方法呼叫關係
  2. 透過遍歷特性分支編譯後的 class 檔案,再透過事件生成器啟動,觸發類/方法篩選器事件
  3. 最終只輸出指定型別方法的呼叫鏈,包括:RPC 介面,HTTP 介面,定時任務,MQ 生產與消費

關鍵程式碼

1. asm構建classReader的方式不僅可以透過已載入的類名指定也可以透過輸入流InputStream),這就使得透過直接遍歷專案編譯過的.class解析呼叫鏈變為可能
FileHelper.getFilePaths(classPath, dir, ".class"); // 遍歷編譯後的build/class路徑下的所有.class檔案
classPath.forEach(c->{
    ClassReader classReader = new ClassReader(new FileInputStream(c));
    ClassSpider classSpider = new ClassSpider(methodInvokeInfos);
    classReader.accept(classSpider, org.objectweb.asm.ClassReader.SKIP_FRAMES);
}

2. 掃描的目的是逆推對外暴露需要回歸的功能例如介面定時器訊息佇列等所以需要排除掉一些無關的鏈路
例如dubbothriftjobnsq在編寫中其類一般都或有特定的註解或有特定的父類或有特定實現的介面型別所以可以在類刪選器classvisiotr中進行篩選
public AnnotationVisitor visitAnnotation(String annotation, boolean b) {
    if (annotation.endsWith("RestController;")) {
        flag = "HTTP"
    }
    return super.visitAnnotation(annotation, b);
}
  • 分支差異對比
  1. 遍歷 master、branch 路徑專案下的所有.java 檔案,生成抽象語法樹,並做去噪處理(空格,註釋等無關改動)
  2. 對比方法(註解,簽名,返回值,以及方法體),統計特性分支改動的方法

關鍵程式碼

1. 先比對有差異的檔案這裡直接比對檔案大小以及是否存在新增的java檔案收攏第二步的篩選範圍
branch.forEach( (rp, b) -> {
    if (!master.containsKey(rp)) {
        b.setStatus(Status.NEW);
    } else {
        JavaFileInfo m = master.get(rp);
        if (b.getLength() != m.getLength()){
            b.setStatus(Status.MODIFY);
            m.setStatus(Status.MODIFY);
        }
    }
});
master.forEach( (rp, m) -> {
    if (!branch.containsKey(rp)) {
        m.setStatus(Status.DELETE);
    }
});

2. 遍歷branch  master路徑下修改過或新增的.java檔案生成AST
CompilationUnit cu = StaticJavaParser.parse(file);
List<Comment> comments = cu.getAllContainedComments();  // 這裡開始去除無關注釋
        List<Comment> unwantedComments = comments  
                .stream()
                .filter(p -> !p.getCommentedNode().isPresent() || p instanceof LineComment)
                .collect(Collectors.toList());
        unwantedComments.forEach(Node::remove);
VoidVisitor<List<ClassParser>> classParserVoidVisitor = new VisitorPrinter();
classParserVoidVisitor.visit(cu, classParsers);  // 遍歷檔案,儲存語法樹

3. 對比差異化輸出特性分支修改/新增的方法
masterMethod.checkAnnotationEqual(branchMethod).checkTypeEqual(branchMethod).checkBodyEqual(bbranchMethod);  // 這裡我主要比對了方法的註解,詳情就不展開了
  • 呼叫鏈&差異化輸出
  1. 遍歷呼叫鏈與上述差異化方法,輸出需要回歸的指定方法/介面
  2. 附帶資訊可包括:統計改動了多少行程式碼,改動的型別(包括:返回值改動,註解改動,新增方法,方法體變更),以及對應改動點
  • 呼叫鏈入庫,並提供介面供查詢 或 回撥特定介面
  1. 可將介面與日常手工迴歸的案例/自動化案例做匹配,這樣精準測試可以提送指定的案例用於迴歸&測試

相關文章