Spring Boot项目Service类单元测试自动生成
在Spring Boot项目中,对Service类进行单元测试对于开发工程师而言具有重大意义和作用:
- 验证业务逻辑的正确性和完整性
- 核心业务逻辑的准确实现:Service类通常包含核心业务逻辑。单元测试确保这些逻辑被正确实现,满足业务需求。
- 处理各种情况:单元测试可以覆盖各种可能的使用情况,包括正常情况和异常情况,确保服务在各种条件下都能正确执行。
促进代码质量和可维护性 - 代码质量:通过单元测试,可以持续监控代码质量,及时发现和修复bug。
重构和代码改进:单元测试为重构和改进代码提供了安全网,帮助开发者在修改代码时保持自信。
- 加速开发和反馈周期
- 快速反馈:单元测试提供即时反馈,帮助开发者快速识别和解决问题。
减少调试时间:当出现问题时,良好的单元测试可以减少用于查找和修复bug的时间。
降低后期维护成本 - 易于维护的代码库:有良好单元测试支持的代码库通常更易于维护和扩展。
文档的作用:单元测试代码本身可以作为某种形式的文档,说明如何使用代码以及代码的预期行为。
- 快速反馈:单元测试提供即时反馈,帮助开发者快速识别和解决问题。
- 促进良好的设计实践
- 鼓励良好的设计:为了便于测试,代码往往会被设计得更加模块化和清晰。
- 依赖注入:Spring Boot鼓励使用依赖注入,这在编写可测试代码时非常有用。
- 支持敏捷和持续集成
- 敏捷开发:单元测试支持敏捷开发实践,如测试驱动开发(TDD)。
- 持续集成:自动化的单元测试是持续集成(CI)的核心部分,确保代码变更不会破坏现有功能。
- 其他功能
- 安全性测试:在编写服务层单元测试时,还可以考虑安全性方面的测试,如权限验证、输入验证等。
- 性能测试:虽然通常不在单元测试的范畴内,但开发者可以通过某些单元测试初步评估代码的性能。
- 集成测试:除了单元测试,还应考虑编写集成测试,以验证服务层组件与数据库、其他服务或API的集成情况。
- 行为驱动开发(BDD):结合行为驱动开发(Behavior-Driven Development)的实践,单元测试可以更贴近业务,提高业务人员和技术人员之间的沟通效率。
单元测试在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(";\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);}
}
优缺点分析
优点
- 环境兼容性强:该工具仅需Java环境即可运行,不依赖于特定的开发环境或额外的软件,强化了其在不同系统环境下的适用性。
- 操作简便:简化操作流程,无需外部网络连接或依赖,提高了工具的可访问性和易用性。
- 高度可定制:提供代码模板定制功能,允许用户根据具体的代码环境和需求进行个性化调整,增加了工具的灵活性。
缺点
- 手动干预需求:自动生成的测试参数可能不符合实际需求,需手动调整,这增加了使用者的工作量。
- 单一类别限制:每次只能生成一个类的单元测试,限制了工具的效率,特别是在处理大型项目时。
- 潜在的重写风险:如果存在同名的单元测试类,新生成的测试类可能会覆盖原有测试,导致数据丢失。
未来可拓展方向
- 批量处理功能:增加按路径批量生成测试类的功能,以减少重复性工作,提高效率。
- 构造方法的灵活性:提供对不同构造方法参数的支持,以适应那些不能仅用空构造方法实例化的类。
- 智能参数填充:根据参数名称,使用生成随机数或适当的随机值进行填充,以更贴近实际使用情况,减少手动调整的需求。
通过这些拓展,工具将更加智能化和自动化,能够更有效地适应复杂的测试环境和多样化的需求。
相关文章:
Spring Boot项目Service类单元测试自动生成
在Spring Boot项目中,对Service类进行单元测试对于开发工程师而言具有重大意义和作用: 验证业务逻辑的正确性和完整性 核心业务逻辑的准确实现:Service类通常包含核心业务逻辑。单元测试确保这些逻辑被正确实现,满足业务需求。处…...
Typescript中 interface 和 type 的区别是什么?
在 TypeScript 中,interface 和 type 都用于定义类型,但它们有一些区别。 1. 语法差异: interface 关键字用于声明接口,使用 interface 可以定义对象的形状、函数的签名等。 type 关键字用于声明类型别名,可以给一个…...
W2311294-万宾科技可燃气体监测仪怎么进行数据监测
万宾科技可燃气体监测仪怎么进行数据监测 燃气是现代城市之中重要的能源,它已经渗透到城市生活的方方面面,对燃气管网的管理也在考验着政府人员的工作能力。燃气管网的安全运行和城市的安全和人民的生活直接挂钩。为了及时掌握燃气管网的运行状态&#x…...
Elasticsearch:向量搜索 (kNN) 实施指南 - API 版
作者:Jeff Vestal 本指南重点介绍通过 HTTP 或 Python 使用 Elasticsearch API 设置 Elasticsearch 以进行近似 k 最近邻 (kNN) 搜索。 对于主要使用 Kibana 或希望通过 UI 进行测试的用户,请访问使用 Elastic 爬虫的语义搜索入门指南。你也可以参考文章…...
704 二分查找 day1
class Solution { public: int search(vector<int>& nums, int target) { int left 0; int right nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right] while (left < right) { // 当leftright,区间[left, right]依然有效&…...
Python面试破解:return和yield的细腻差别
更多Python学习内容:ipengtao.com 大家好,我是涛哥,今天为大家分享 Python面试破解:return和yield的细腻差别,全文3000字,阅读大约10钟。 在Python的函数编程中,return和yield是两个常用的关键词…...
云时空社会化商业 ERP 系统 service SQL 注入漏洞复现
0x01 产品简介 时空云社会化商业ERP(简称时空云ERP) ,该产品采用JAVA语言和Oracle数据库, 融合用友软件的先进管理理念,汇集各医药企业特色管理需求,通过规范各个流通环节从而提高企业竞争力、降低人员成本…...
Vue3-Pinia
Pinia是什么 Pinia是Vue的最新状态管理工具,是Vuex的替代品 比Vuex更大的优势在于: 1.提供更加简单的API(去掉了mutation) 2.提供符合,组合式风格的API(和Vue3新语法统一) 3.去掉了modules…...
数据挖掘之时间序列分析
一、 概念 时间序列(Time Series) 时间序列是指同一统计指标的数值按其发生的时间先后顺序排列而成的数列(是均匀时间间隔上的观测值序列)。 时间序列分析的主要目的是根据已有的历史数据对未来进行预测。 时间序列分析主要包…...
iOS NSDate的常用API
目录 一、创建日期 1.获取当前时间 2.当前时间指定秒数之后/前的时间 3.指定日期之后/后的时间 4.2001年之后/前指定秒数的时间 5.1970年之后/后指定秒数的时间 二、初始化日期 1.init 2.时间间指定秒数的时间 3.指定时间指定秒数之前/后的时间 4.2001年指定秒数之后…...
谱方法学习笔记-下(超详细)
谱方法学习笔记📒 谱方法学习笔记-上(超详细) 声明:鉴于CSDN使用 K a T e X KaTeX KaTeX 渲染公式, KaTeX \KaTeX KATEX 与 L a T e X LaTeX LaTeX 不同,不支持直接的交叉引用命令,如\label和\eqref。 KaTeX \KaT…...
iOS--UIPickerView学习
UIPickerView 使用场景和功能UIPickerView遵循代理协议和数据源协议创建对象,添加代理必须实现的代理方法非必要实现的方法demo用到的其他函数提示 效果展示 使用场景和功能 UIPickerView 最常见的用途是作为选项选择器,允许用户从多个选项中选择一个。…...
Docker安装Elasticsearch以及ik分词器
Elasticsearch 是一个分布式、RESTful 风格的搜索和数据分析引擎,能够解决不断涌现出的各种用例。作为 Elastic Stack 的核心,Elasticsearch 会集中存储您的数据,让您飞快完成搜索,微调相关性,进行强大的分析ÿ…...
[架构之路-254]:目标系统 - 设计方法 - 软件工程 - 软件设计 - 架构设计 - 全程概述
目录 一、软件架构概述 1.1 什么是软件架构 1.2 为什么需要软件架构设计 1.3 软件架构设计在软件设计中位置 (1)软件架构设计(层次划分、模块划分、职责分工): (2)软件高层设计、概要设计…...
centos7上源码安装mysql--运维高级
第一步,安装必要的依赖: yum install -y cmake ncurses-devel bison gcc gcc-c make unzip libaio numactl 第二步,创建mysql用户和组: wget http://dev.mysql.com/get/Downloads/MySQL-5.7/mysql-5.7.18.tar.gz tar zxvf mysql-5.7.18.tar.gz 第三步,下载MySQL 5.7.18 源码…...
Linux小程序之进度条
> 作者简介:დ旧言~,目前大二,现在学习Java,c,c,Python等 > 座右铭:松树千年终是朽,槿花一日自为荣。 > 目标:自己能实现进度条 > 毒鸡汤: > …...
Grafana采用Nginx反向代理
一、场景介绍 在常规操作中,一般情况下不会放开许多端口给外部访问,特别是直接 ip:port 的方式开放访问。但是 Grafana 的请求方式在默认情况下是没有任何规律可寻的。 为了满足业务需求(后续通过 Nginx 统一一个接口暴露 N 个服务…...
Python接口自动化测试如何设计接口测试用例(详解)
简介 上篇我们已经介绍了什么是接口测试和接口测试的意义。在开始接口测试之前,我们来想一下,如何进行接口测试的准备工作。或者说,接口测试的流程是什么?有些人就很好奇,接口测试要流程干嘛?不就是拿着接口…...
Spring不再支持Java8了
在今天新建模块的时候发现了没有java8的选项了,结果一查发现在11月24日,Spring不再支持8了,这可怎么办呢?我们可以设置来源为阿里云https://start.aliyun.com/ 。 java8没了 设置URL为阿里云的地址...
Android 实现APP可切换多语言
如果是单独给app加上国际化,其实很容易,创建对应的国家资源文件夹即可,如values-en,values-pt,app会根据当前系统语言去使用对应语言资源文件,如果找不到,则使用values文件夹里的资源 但本文讲得是另外一种情况,就是app内置一个切换多语言的页面,可以给用户切换 步骤 1.添加服务…...
eNSP-Cloud(实现本地电脑与eNSP内设备之间通信)
说明: 想象一下,你正在用eNSP搭建一个虚拟的网络世界,里面有虚拟的路由器、交换机、电脑(PC)等等。这些设备都在你的电脑里面“运行”,它们之间可以互相通信,就像一个封闭的小王国。 但是&#…...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...
CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
(转)什么是DockerCompose?它有什么作用?
一、什么是DockerCompose? DockerCompose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器。 Compose文件是一个文本文件,通过指令定义集群中的每个容器如何运行。 DockerCompose就是把DockerFile转换成指令去运行。 …...
关键领域软件测试的突围之路:如何破解安全与效率的平衡难题
在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件,这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下,实现高效测试与快速迭代?这一命题正考验着…...
Chrome 浏览器前端与客户端双向通信实战
Chrome 前端(即页面 JS / Web UI)与客户端(C 后端)的交互机制,是 Chromium 架构中非常核心的一环。下面我将按常见场景,从通道、流程、技术栈几个角度做一套完整的分析,特别适合你这种在分析和改…...
深度学习之模型压缩三驾马车:模型剪枝、模型量化、知识蒸馏
一、引言 在深度学习中,我们训练出的神经网络往往非常庞大(比如像 ResNet、YOLOv8、Vision Transformer),虽然精度很高,但“太重”了,运行起来很慢,占用内存大,不适合部署到手机、摄…...
uni-app学习笔记三十五--扩展组件的安装和使用
由于内置组件不能满足日常开发需要,uniapp官方也提供了众多的扩展组件供我们使用。由于不是内置组件,需要安装才能使用。 一、安装扩展插件 安装方法: 1.访问uniapp官方文档组件部分:组件使用的入门教程 | uni-app官网 点击左侧…...
Mysql故障排插与环境优化
前置知识点 最上层是一些客户端和连接服务,包含本 sock 通信和大多数jiyukehuduan/服务端工具实现的TCP/IP通信。主要完成一些简介处理、授权认证、及相关的安全方案等。在该层上引入了线程池的概念,为通过安全认证接入的客户端提供线程。同样在该层上可…...
