当前位置:   article > 正文

Spring Boot项目Service类单元测试自动生成_单元测试 service

单元测试 service

在Spring Boot项目中,对Service类进行单元测试对于开发工程师而言具有重大意义和作用:

  • 验证业务逻辑的正确性和完整性
    • 核心业务逻辑的准确实现:Service类通常包含核心业务逻辑。单元测试确保这些逻辑被正确实现,满足业务需求。
    • 处理各种情况:单元测试可以覆盖各种可能的使用情况,包括正常情况和异常情况,确保服务在各种条件下都能正确执行。
      促进代码质量和可维护性
    • 代码质量:通过单元测试,可以持续监控代码质量,及时发现和修复bug。
      重构和代码改进:单元测试为重构和改进代码提供了安全网,帮助开发者在修改代码时保持自信。
  • 加速开发和反馈周期
    • 快速反馈:单元测试提供即时反馈,帮助开发者快速识别和解决问题。
      减少调试时间:当出现问题时,良好的单元测试可以减少用于查找和修复bug的时间。
      降低后期维护成本
    • 易于维护的代码库:有良好单元测试支持的代码库通常更易于维护和扩展。
      文档的作用:单元测试代码本身可以作为某种形式的文档,说明如何使用代码以及代码的预期行为。
  • 促进良好的设计实践
    • 鼓励良好的设计:为了便于测试,代码往往会被设计得更加模块化和清晰。
    • 依赖注入:Spring Boot鼓励使用依赖注入,这在编写可测试代码时非常有用。
  • 支持敏捷和持续集成
    • 敏捷开发:单元测试支持敏捷开发实践,如测试驱动开发(TDD)。
    • 持续集成:自动化的单元测试是持续集成(CI)的核心部分,确保代码变更不会破坏现有功能。
  • 其他功能
    • 安全性测试:在编写服务层单元测试时,还可以考虑安全性方面的测试,如权限验证、输入验证等。
    • 性能测试:虽然通常不在单元测试的范畴内,但开发者可以通过某些单元测试初步评估代码的性能。
    • 集成测试:除了单元测试,还应考虑编写集成测试,以验证服务层组件与数据库、其他服务或API的集成情况。
    • 行为驱动开发(BDD):结合行为驱动开发(Behavior-Driven Development)的实践,单元测试可以更贴近业务,提高业务人员和技术人员之间的沟通效率。

单元测试在Spring Boot项目中扮演着至关重要的角色,对于确保代码质量、加速开发过程、降低维护成本以及推动良好的开发实践具有显著影响。

背景

由于所在公司的代码环境切换至内部网络,现有的插件用于生成单元测试变得不再适用。为了解决这一挑战,提高工作效率,我开发了一个单元测试生成Java工具类,专门用于自动生成服务类的单元测试代码。
代码框架:

依赖版本
Spring Boot2.7.12
JUnit5.8.2

目标

我们的主要目标是创建一个尽可能完善的Spring Boot单元测试方法生成器,以减少重复工作并提高工作效率。

实现效果

我们的工具类具备以下特点:

  • 为每个服务方法自动生成对应的请求和响应类。
  • 全面支持原始类型、类类型参数以及枚举类型参数的请求和响应。
  • 当方法参数是类类型时,使用空构造函数进行实例化。
  • 对于常见的基础类型、包装类型和枚举类型,自动设置默认值。
  • 自动打印每个方法的响应结果,以便于调试和验证。

这个工具类的开发旨在提升测试代码的编写效率,同时保持测试覆盖率的完整性,从而避免在单元测试编写方面重复“造轮子”。

代码实现


import java.io.*;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.util.*;


public class TestClassAutoGenerator {

    // JAVA保留字
    private static final List<String> keywords = Arrays.asList("abstract", "assert", "boolean", "break", "byte", "case", "catch", "char", "class", "const",
            "continue", "default", "do", "double", "else", "enum", "extends", "final", "finally", "float",
            "for", "goto", "if", "implements", "import", "instanceof", "int", "interface", "long", "native",
            "new", "package", "private", "protected", "public", "return", "short", "static", "strictfp", "super",
            "switch", "synchronized", "this", "throw", "throws", "transient", "try", "void", "volatile", "while");
    private static final String javatest = "/src/test/java/";

    // 创建目录
    public static void createDirectoryIfNeeded(String filePath) {
        File file = new File(filePath);
        File directory = file.getParentFile();

        if (directory != null && !directory.exists()) {
            // 如果目录不存在,则创建它
            boolean isCreated = directory.mkdirs();
            if (isCreated) {
                System.out.println("目录已创建: " + directory.getAbsolutePath());
            } else {
                System.out.println("目录创建失败: " + directory.getAbsolutePath());
            }
        } else {
            assert directory != null;
            System.out.println("目录已存在: " + directory.getAbsolutePath());
        }
    }

    // 主体方法:按service类在指定项目下自动生成service类 
    public void generateTestForClass(String outputPath, Class<?> serviceClass) {

        String packagePath = serviceClass.getPackage().getName().replace(".","/");
        // 生成路径
        outputPath = outputPath+javatest+packagePath;
        String className = serviceClass.getSimpleName();
        String testClassName = className + "Test";
        // 测试类的代码内容
        String content = generateTestClassContent(serviceClass, testClassName);
        createDirectoryIfNeeded(outputPath + "/" + testClassName + ".java");
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(outputPath + "/" + testClassName + ".java"))) {
            writer.write(content);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 测试类的代码生成
    private String generateTestClassContent(Class<?> serviceClass, String testClassName) {
        StringBuilder classContent = new StringBuilder();
        classContent.append("package ").append(serviceClass.getPackage().getName()).append(";\n\n");

        // 导入请求响应包
        Set<String> imports = new HashSet<>();
        for (Method method : serviceClass.getDeclaredMethods()) {
            Class<?>[] paramTypes = method.getParameterTypes();
            for (Class<?> paramType : paramTypes) {
                if (paramType.getPackage() != null && !imports.contains(paramType.getPackage().getName() + "." + paramType.getSimpleName())) {
                    classContent.append("import ").append(paramType.getPackage().getName()).append(".").append(paramType.getSimpleName()).append(";\n");
                    imports.add(paramType.getPackage().getName() + "." + paramType.getSimpleName());
                }
            }
            Class<?> returnType = method.getReturnType();

            if (returnType.getPackage() !=null && !imports.contains(returnType.getPackage().getName()+"."+returnType.getSimpleName())) {
                classContent.append("import ").append(returnType.getPackage().getName()).append(".").append(returnType.getSimpleName()).append(";\n");
            }
        }


        // 导入SpringBoot项目运行测试所需的包
        classContent
                .append("import lombok.extern.slf4j.Slf4j;\n")
                .append("import ").append(serviceClass.getPackage().getName()).append(".").append(serviceClass.getSimpleName()).append(";\n")
                .append("import org.junit.jupiter.api.Test;\n")
                .append("import org.springframework.boot.test.context.SpringBootTest;\n")
                .append("import com.alibaba.fastjson.JSON;\n")
                .append("import org.springframework.beans.factory.annotation.Autowired;\n\n")
                .append("@Slf4j\n")
                .append("@SpringBootTest\n")
                .append("public class ").append(testClassName).append(" {\n\n")
                .append("    @Autowired\n")
                .append("    private ").append(serviceClass.getSimpleName()).append(" ")
                .append(toCamelCase(serviceClass.getSimpleName())).append(";\n\n");

        // 遍历生成单元测试
        for (Method method : serviceClass.getDeclaredMethods()) {
            if (Modifier.isPublic(method.getModifiers())) {

                classContent.append("    @Test\n")
                        .append("    public void test").append(capitalizeFirstLetter(method.getName()))
                        .append("() throws Exception {\n")
                        .append(generateMethodTestLogic(method,serviceClass))
                        .append("    }\n\n");
            }
        }

        classContent.append("}\n");
        return classContent.toString();
    }

    // 生成单元测试代码
    private String generateMethodTestLogic(Method method,Class<?> serviceClass) {
        StringBuilder testLogic = new StringBuilder();
        testLogic.append("        // Test logic for ").append(method.getName()).append("\n");

        Class<?>[] paramTypes = method.getParameterTypes();
        Class<?> returnType = method.getReturnType();
        List<String> params = new ArrayList<>();
        Hashtable<String, Integer> paramCount = new Hashtable<>();
        for (Class<?> paramType : paramTypes) {
            String param = getParamName(paramType, paramCount);
            testLogic.append("        ").append(paramType.getSimpleName()).append(" ")
                    .append(param).append("=");
            testLogic.append(getDefaultValueForType(paramType));
            testLogic.append(";\n");
            params.add(param);
            if (getDefaultValueForType(paramType).startsWith("new")) {
                testLogic.append("        //TODO set params for ").append(toCamelCase(paramType.getSimpleName())).append("\n\n");
            }
        }

        testLogic.append("        ");
        if (returnType.getPackage()!=null) {
            testLogic.append(returnType.getSimpleName()).append(" response = ");
        }
        testLogic.append(toCamelCase(serviceClass.getSimpleName()))
                .append(".").append(method.getName()).append("(");

        for (int i = 0; i < paramTypes.length; i++) {
            testLogic.append(params.get(i));
            if (i < paramTypes.length - 1) {
                testLogic.append(", ");
            }
        }
        testLogic.append(");\n");

        if (returnType.getPackage()!=null) {
            testLogic.append("        log.info(\"Response: \" + JSON.toJSONString(response));\n");
        }

        return testLogic.toString();
    }

    private String getParamName(Class<?> paramType,Hashtable<String, Integer> paramCount) {
        String name = paramType.getSimpleName();
        String init = "arg";
        if (paramType.isPrimitive() ) {
            if (paramType.equals(boolean.class)) {
                init = "flag";
            }
        } else if (paramType.equals(String.class)) {
            init = "s";
        } else {
            init =toCamelCase(name);
        }
        if (keywords.contains(init)) {
            init =init.substring(0,1);
        }
        if (paramCount.get(init)==null) {
            paramCount.put(init,1);
            return init;
        } else {
            paramCount.replace(init,paramCount.get(init)+1);
            return init+(paramCount.get(init));
        }
    }

    // 生成默认值
    private String getDefaultValueForType(Class<?> type) {
        if (type.isPrimitive()) {
            if (type.equals(boolean.class)) {
                return "false";
            } else if (type.equals(long.class)) {
                return "0L";
            }else if (type.equals(float.class)) {
                return "0F";
            }else if (type.equals(double.class)) {
                return "0D";
            }
            return "0";
        } else if (type.equals(String.class)) {
            return "\"\"";
        } else if (type.equals(Long.class)) {
            return "0L";
        } else if (type.equals(Float.class)) {
            return "0F";
        } else if (type.equals(Double.class)) {
            return "0D";
        } else if (type.equals(Short.class) || type.equals(Integer.class)) {
            return "0";
        } else if (type.equals(BigDecimal.class)) {
            return "new " + type.getSimpleName() + "(\"0\")";
        } else if (type.isEnum()) {
            return type.getSimpleName()+"."+type.getEnumConstants()[0].toString();
        }
        else {
            return "new " + type.getSimpleName() + "()";
        }
    }

    private String toCamelCase(String str) {
        return Character.toLowerCase(str.charAt(0)) + str.substring(1);
    }

    private String capitalizeFirstLetter(String str) {
        return Character.toUpperCase(str.charAt(0)) + str.substring(1);
    }
    // 程序入口
    public static void main(String[] args) {
        TestClassAutoGenerator generator = new TestClassAutoGenerator();

        // 为单一类生成单元测试
        generator.generateTestForClass("XX-app-service(换成你的单元测试所在项目名称)", XXService.class);
    }
}



  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138
  • 139
  • 140
  • 141
  • 142
  • 143
  • 144
  • 145
  • 146
  • 147
  • 148
  • 149
  • 150
  • 151
  • 152
  • 153
  • 154
  • 155
  • 156
  • 157
  • 158
  • 159
  • 160
  • 161
  • 162
  • 163
  • 164
  • 165
  • 166
  • 167
  • 168
  • 169
  • 170
  • 171
  • 172
  • 173
  • 174
  • 175
  • 176
  • 177
  • 178
  • 179
  • 180
  • 181
  • 182
  • 183
  • 184
  • 185
  • 186
  • 187
  • 188
  • 189
  • 190
  • 191
  • 192
  • 193
  • 194
  • 195
  • 196
  • 197
  • 198
  • 199
  • 200
  • 201
  • 202
  • 203
  • 204
  • 205
  • 206
  • 207
  • 208
  • 209
  • 210
  • 211
  • 212
  • 213
  • 214
  • 215
  • 216
  • 217
  • 218
  • 219
  • 220
  • 221
  • 222
  • 223
  • 224
  • 225
  • 226
  • 227

优缺点分析

优点

  1. 环境兼容性强:该工具仅需Java环境即可运行,不依赖于特定的开发环境或额外的软件,强化了其在不同系统环境下的适用性。
  2. 操作简便:简化操作流程,无需外部网络连接或依赖,提高了工具的可访问性和易用性。
  3. 高度可定制:提供代码模板定制功能,允许用户根据具体的代码环境和需求进行个性化调整,增加了工具的灵活性。

缺点

  1. 手动干预需求:自动生成的测试参数可能不符合实际需求,需手动调整,这增加了使用者的工作量。
  2. 单一类别限制:每次只能生成一个类的单元测试,限制了工具的效率,特别是在处理大型项目时。
  3. 潜在的重写风险:如果存在同名的单元测试类,新生成的测试类可能会覆盖原有测试,导致数据丢失。

未来可拓展方向

  • 批量处理功能:增加按路径批量生成测试类的功能,以减少重复性工作,提高效率。
  • 构造方法的灵活性:提供对不同构造方法参数的支持,以适应那些不能仅用空构造方法实例化的类。
  • 智能参数填充:根据参数名称,使用生成随机数或适当的随机值进行填充,以更贴近实际使用情况,减少手动调整的需求。

通过这些拓展,工具将更加智能化和自动化,能够更有效地适应复杂的测试环境和多样化的需求。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小蓝xlanll/article/detail/653879
推荐阅读
相关标签
  

闽ICP备14008679号