java 代码给单元测试方法添加断言(多线程版本)

 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();
        ExecutorService executorService = Executors.newFixedThreadPool(4); // 根据实际情况调整线程池大小
        List<Future<?>> futures = new ArrayList<>();

        for (String filePath : packInfoSet) {
            Callable<Void> task = () -> {
                try {
                    processFile(filePath);
                    System.out.println("File processed successfully.");
                    count.incrementAndGet();
                    printSummary();
                    return null;
                } catch (IOException e) {
                    errCount.incrementAndGet();
                    printSummary();
                    System.err.println("An error occurred: " + e.getMessage());
                    throw new RuntimeException(e);
                }

            };
            futures.add(executorService.submit(task));

        }

        // 等待所有任务完成
        for (Future<?> future : futures) {
            future.get();
        }

        executorService.shutdown(); // 关闭线程池
    }

 private static void printSummary() {
        // 这里可以直接打印count和errCount,因为在调用此方法前所有任务已完成
        int processedCount = count.get();
        int errorCount = errCount.get();

        int remainingTasks = processedCount + errorCount;
        double remainingPercentage = ((double)remainingTasks / packInfoSet.size()) * 100;

        System.out.println("\n******************************\n"
                + "总条数:" + packInfoSet.size()
                + " 处理条数:" + remainingTasks
                + "  成功处理条数:" + processedCount
                + "\n剩余条数:" + (packInfoSet.size() - processedCount)
                + "  出错条数:" + errorCount
                + "  进度条:" + 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);
    }

  • 23
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

潘涛智码工坊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值