1.项目场景:
面对一个庞大且历史悠久的项目,单元测试的有效性低下(不足10%),急需提升测试质量,于是上头临时安排个任务,下一周开会时要求断言有效率要提升到90%,这时相信各位小伙伴们心里已经一万个策马奔腾........这里小哥就不绕关子直接上代码了。
2.解决方案:
利用JavaParser库解析Java源码,识别出带有@Test注解的方法,并检查这些方法是否已含有断言。对于缺少断言的方法,在方法末尾自动追加
assertTrue(true);
作为基本的断言,确保测试覆盖率的统计更加准确。
3.实现细节:
- 初始化阶段:读取配置文件,获取待处理的测试类信息。
- 处理文件:逐个解析测试类文件,检查并修改。
- 断言追加逻辑:在测试方法末尾追加默认断言,同时处理包导入逻辑。
*4.注意: projectTestJavaPath 这个变量配置你测试类在哪个包下,这里写的时绝对路径:“***D:\data\stores\work\git\testProject\src\test\java\**”
configPath 这个变量是一个配置文件,这里的配置来自jinkens:
格式长这样子:
com.csair.test.TestCaseServletZhougrTest#base
com.csair.test.TestCaseServletZhougrTest#AirportPickupService
com.csair.test.TestCaseServletZhougrTest#grouponVo
com.csair.test.TestCaseServletZhougrTest#supply
com.csair.test.TestPoJoEnumCaiYouLinTest#testPoJo
大家可以根据自己的实际情况来修改。
pom.xml
在pom中增加javaparser依赖快速帮我们解析java文件。
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-symbol-solver-core</artifactId>
<version>3.23.1</version> <!-- 检查最新版本 -->
</dependency>
<dependency>
<groupId>com.github.javaparser</groupId>
<artifactId>javaparser-core</artifactId>
<version>3.23.1</version> <!-- 与上面的版本保持一致 -->
</dependency>
java代码实现:
新增 : TestFileModifier.class
代码:
package com.example.nfsc.service;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.ImportDeclaration;
import com.github.javaparser.ast.NodeList;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.stmt.BlockStmt;
import com.github.javaparser.ast.stmt.Statement;
import com.github.javaparser.ast.visitor.GenericVisitorAdapter;
import com.github.javaparser.ast.visitor.ModifierVisitor;
import com.github.javaparser.ast.visitor.Visitable;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Stream;
public class TestFileModifier {
private static Set<String> packInfoSet = new HashSet<>(16);
//这个是项目存放javac测试类的路径
final static private String projectTestJavaPath = "D:\\data\\stores\\work\\git\\testProject\\src\\test\\java\\";
final static private String configPath = "D:\\data\\UnitTestAssertConf.txt";
static private AtomicInteger count = new AtomicInteger(0);
static private AtomicInteger errCount = new AtomicInteger(0);
public static void main(String[] args) {
init();
for (String filePath : packInfoSet) {
try {
processFile(filePath);
System.out.println("File processed successfully.");
deleteBackupIfNoErrors(filePath); // 确认无错误后删除备份
count.incrementAndGet();
} catch (IOException e) {
errCount.incrementAndGet();
System.err.println("An error occurred: " + e.getMessage());
}
int remainingTasks = (count.get() + errCount.get());
double remainingPercentage = ((double) remainingTasks / packInfoSet.size()) * 100;
System.out.println("\n******************************\n"
+ "总条数:" + packInfoSet.size()
+ " 处理条数:" + (count.get() + errCount.get())
+ " 成功处理条数:" + count.get()
+ "\n剩余条数:" + (packInfoSet.size() - count.get())
+ " 出错条数:" + (errCount.get())
+ " 进度条:" + String.format("%.2f%%", remainingPercentage)
+ "\n******************************\n"
);
}
}
static private void init() {
try (Stream<String> lines = Files.lines(Paths.get(configPath), StandardCharsets.UTF_8)) {
// 使用forEach处理每行,避免一次性加载所有数据到内存
lines.forEach(TestFileModifier::processLargeFileLine);
} catch (IOException e) {
e.printStackTrace();
System.err.println("Error reading file: " + e.getMessage());
}
}
private static void processLargeFileLine(String line) {
// System.out.println(line);
if (null == line || 0 == line.trim().length()) {
return;
}
String[] unitTestClassAndMethodInfo = line.split("#");
if (2 != unitTestClassAndMethodInfo.length) {
//无效
return;
}
String testClassPackPath = unitTestClassAndMethodInfo[0];
//转换成文件路径
testClassPackPath = testClassPackPath.replace(".", "\\");
//将项目路径拼接上
String testJavaFilePath = projectTestJavaPath + testClassPackPath;
packInfoSet.add(testJavaFilePath + ".java");
}
private static void processFile(String filePath) throws IOException {
File originalFile = new File(filePath);
File backupFile = new File(filePath + ".bak");
// 创建备份文件
Files.copy(originalFile.toPath(), backupFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
CompilationUnit cu = StaticJavaParser.parse(originalFile);
new TestMethodAppender().visit(cu, null);
// 写回修改后的内容
Files.write(originalFile.toPath(), cu.toString().getBytes());
}
// 安全删除备份文件,仅在主流程无异常时调用
private static void deleteBackupIfNoErrors(String filePath) {
File backupFile = new File(filePath + ".bak");
if (backupFile.exists()) {
if (!backupFile.delete()) {
System.err.println("Failed to delete the backup file.");
} else {
System.out.println("Backup file deleted.");
}
}
}
static class TestMethodAppender extends ModifierVisitor<Void> {
@Override
public Visitable visit(MethodDeclaration n, Void arg) {
if (hasTestAnnotation(n)) {
ensureAssertionImported(n.findCompilationUnit().orElseThrow(() -> new NoSuchElementException("CompilationUnit not found")));
appendAssertTrueIfNeeded(n.getBody().orElse(null)); // 确保不会重复添加
}
return super.visit(n, arg);
}
private boolean hasTestAnnotation(MethodDeclaration method) {
return method.getAnnotations().stream()
.anyMatch(annotation -> annotation.getNameAsString().equals("Test"));
}
private void ensureAssertionImported(CompilationUnit compilationUnit) {
if (!isAssertionImported(compilationUnit)) {
addAssertionImport(compilationUnit);
}
}
//判断包是否导入
private boolean isAssertionImported(CompilationUnit compilationUnit) {
NodeList<ImportDeclaration> imports = compilationUnit.getImports();
// 检查是否已有org.junit.Assert的任何形式的导入
return imports.stream().anyMatch(importDec ->
importDec.getNameAsString().equals("org.junit.Assert")
);
}
//引入包
private void addAssertionImport(CompilationUnit compilationUnit) {
// 确保不会重复添加
if (!isAssertionImported(compilationUnit)) {
compilationUnit.addImport("org.junit.Assert");
}
}
//判断最后一行是否有这个段代码,没有的话将这段代码新增进去
private void appendAssertTrueIfNeeded(BlockStmt body) {
if (body != null && !containsAssertTrue(body)) {
body.addStatement("assertTrue(true);");
}
}
//判断方法里面最后一行是否有这样代码
private boolean containsAssertTrue(BlockStmt body) {
if (body == null || body.getStatements().isEmpty()) {
return false;
}
Statement lastStatement = body.getStatements().get(body.getStatements().size() - 1);
String lastStatementStr = lastStatement.toString();
return lastStatementStr.endsWith("Assert.assertTrue(true);");
}
}
}
问题:
如果测试中存在了:org.junit.Assert.*; 的包,则不会再导入org.junit.Assert;包进入测试类。为了解决这个问题,我们可以这么做:
1.将这个方法的代码修改一下:
//判断最后一行是否有这个段代码,没有的话将这段代码新增进去
private void appendAssertTrueIfNeeded(BlockStmt body) {
if (body != null && !containsAssertTrue(body)) {
body.addStatement("org.junit.Assert.assertTrue(true);");
}
}
2.这的一行代码也注释掉:
static class TestMethodAppender extends ModifierVisitor<Void> {
@Override
public Visitable visit(MethodDeclaration n, Void arg) {
if (hasTestAnnotation(n)) {
//ensureAssertionImported(n.findCompilationUnit().orElseThrow(() -> new NoSuchElementException("CompilationUnit not found")));
appendAssertTrueIfNeeded(n.getBody().orElse(null)); // 确保不会重复添加
}
return super.visit(n, arg);
}
版权归原作者 潘涛智码工坊 所有, 如有侵权,请联系我们删除。