精準測試:基於 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. 可將介面與日常手工迴歸的案例/自動化案例做匹配,這樣精準測試可以提送指定的案例用於迴歸&測試

相關文章