本文介绍: 由于所在公司的代码环境切换至内部网络,现有的插件用于生成单元测试变得不再适用。为了解决这一挑战,提高工作效率,我开发了一个单元测试生成Java工具类,专门用于自动生成服务类的单元测试代码。
在Spring Boot项目中,对Service类进行单元测试对于开发工程师而言具有重大意义和作用:
单元测试在Spring Boot项目中扮演着至关重要的角色,对于确保代码质量、加速开发过程、降低维护成本以及推动良好的开发实践具有显著影响。
背景
由于所在公司的代码环境切换至内部网络,现有的插件用于生成单元测试变得不再适用。为了解决这一挑战,提高工作效率,我开发了一个单元测试生成Java工具类,专门用于自动生成服务类的单元测试代码。
代码框架:
依赖 | 版本 |
---|---|
Spring Boot | 2.7.12 |
JUnit | 5.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(";nn");
// 导入请求响应包
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;nn")
.append("@Slf4jn")
.append("@SpringBootTestn")
.append("public class ").append(testClassName).append(" {nn")
.append(" @Autowiredn")
.append(" private ").append(serviceClass.getSimpleName()).append(" ")
.append(toCamelCase(serviceClass.getSimpleName())).append(";nn");
// 遍历生成单元测试
for (Method method : serviceClass.getDeclaredMethods()) {
if (Modifier.isPublic(method.getModifiers())) {
classContent.append(" @Testn")
.append(" public void test").append(capitalizeFirstLetter(method.getName()))
.append("() throws Exception {n")
.append(generateMethodTestLogic(method,serviceClass))
.append(" }nn");
}
}
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("nn");
}
}
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);
}
}
优缺点分析
优点
- 环境兼容性强:该工具仅需Java环境即可运行,不依赖于特定的开发环境或额外的软件,强化了其在不同系统环境下的适用性。
- 操作简便:简化操作流程,无需外部网络连接或依赖,提高了工具的可访问性和易用性。
- 高度可定制:提供代码模板定制功能,允许用户根据具体的代码环境和需求进行个性化调整,增加了工具的灵活性。
缺点
- 手动干预需求:自动生成的测试参数可能不符合实际需求,需手动调整,这增加了使用者的工作量。
- 单一类别限制:每次只能生成一个类的单元测试,限制了工具的效率,特别是在处理大型项目时。
- 潜在的重写风险:如果存在同名的单元测试类,新生成的测试类可能会覆盖原有测试,导致数据丢失。
未来可拓展方向
- 批量处理功能:增加按路径批量生成测试类的功能,以减少重复性工作,提高效率。
- 构造方法的灵活性:提供对不同构造方法参数的支持,以适应那些不能仅用空构造方法实例化的类。
- 智能参数填充:根据参数名称,使用生成随机数或适当的随机值进行填充,以更贴近实际使用情况,减少手动调整的需求。
通过这些拓展,工具将更加智能化和自动化,能够更有效地适应复杂的测试环境和多样化的需求。
原文地址:https://blog.csdn.net/BIG_friend/article/details/134691942
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_48924.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。