0


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();
        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);
    }

标签: 单元测试

本文转载自: https://blog.csdn.net/csdn466412618/article/details/126881033
版权归原作者 潘涛智码工坊 所有, 如有侵权,请联系我们删除。

“java 代码给单元测试方法添加断言(单线程版本)”的评论:

还没有评论