junit-generator Junit 单元测试生成工具Ma
junit-generator
介绍
一个基于JUnit,Freemarker,Mockito,Maven等技术实现的单元测试类脚手架生成工具Maven插件。
需求
我们在测试驱动开发过程中,总会写一大堆与业务无关的模板式的代码,为了减少开发者写单元测试的工作量,需要一个单元测试类脚手架代码的生成工具。
类关系图
类关系图主要技术说明
-
Maven插件开发:见官网:http://maven.apache.org/guides/plugin/guide-java-report-plugin-development.html
-
XML-DTD 约束文件定义:DTD 的目的是定义 XML 文档的结构,它使用一系列合法的元素来定义文档结构:详解见:
https://www.cnblogs.com/mengdd/archive/2013/05/30/3107361.html
-
FreeMarker模板引擎:中文官方参考手册:http://freemarker.foofun.cn/
-
spi插件机制:见:https://gitee.com/javacoo/xkernel
安装教程
-
配置pom
在测试工程的pom.xml文件中添加如下配置:
<build> <plugins> <plugin> <groupId>com.javacoo</groupId> <artifactId>junit-generator-maven-plugin</artifactId> <version>1.1.0-SNAPSHOT</version> <configuration> <!-- 是否覆盖 --> <overwrite>false</overwrite> <!-- 是否备份--> <backup>true</backup> <!-- 配置文件路径 --> <configurationFile>src/test/resources/junitGeneratorConfig.xml</configurationFile> <!-- 需要执行的上下文ID,多个逗号分隔 --> <contexts>testContext,springTestContext</contexts> </configuration> </plugin> </plugins> </build>
-
添加配置文件:junitGeneratorConfig.xml
在项目resources目录下添加junitGeneratorConfig.xml配置文件:如
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE generatorConfiguration PUBLIC "-//javacoo.com//DTD Junit Generator Configuration 1.0//EN" "http://javacoo.com/dtd/junit-generator-config_1_0.dtd" > <!--junit生成配置--> <generatorConfiguration> <context id="TestContext"> <!--junit 模板配置--> <template templatePath="/template" templateName="test.ftl" templateHandlerName="spring"></template> <!--junit 生成目标类集合--> <classList> <class className="com.javacoo.junit.generator.api.TestApi"/> </classList> </context> <context id="springTestContext"> <!--junit 模板配置--> <template templatePath="/template" templateName="test.ftl" templateHandlerName="spring"></template> <!--junit 生成目标类集合--> <classList> <class className="com.javacoo.junit.generator.api.SpringTestApi"/> </classList> </context> <context id="junit5DefaultContext"> <!--junit 模板配置--> <template templateHandlerName="defaultJUnit5"></template> <!--junit 生成目标类集合--> <classList> <class className="com.javacoo.junit.generator.api.Junit5TestApi"/> </classList> </context> </generatorConfiguration>
-
生成测试代码:
在IDE工具栏查看安装好插件,点击运行,如:
或者执行命令:mvn com.javacoo:junit-generator-maven-plugin:1.1.0-SNAPSHOT:generate
- 生成结果:默认在测试工程 src/test/java 目录生成测试类包名文件夹及测试类,如:
使用说明
-
pom.xml 配置说明
junit-generator-maven-plugin按照标准Maven插件配置即可。
参数说明:
skip:是否跳过生成>非必填,是指是否跳过生成测试类文件,默认为false,不跳过,即生成。
overwrite:是否覆盖->非必填,是指是否覆盖已有的测试类文件,默认为false,不覆盖,即合并。
backup:是否备份->非必填,是指生成测试类前是否备份已有文件,默认为false,不备份(overwrite 为 false 时生效)。
contexts:需要执行的上下文节点,多个以逗号分隔->非必填,junitGeneratorConfig.xml中context节点id configurationFile:配置文件路径->必填,相对测试项目根目录
-
junitGeneratorConfig.xml配置说明
第一行为标准XML文件定义:
<?xml version="1.0" encoding="UTF-8"?>
第二行为junit-generator-maven-plugin特有DTD文件约束说明:
<!DOCTYPE generatorConfiguration PUBLIC "-//javacoo.com//DTD Junit Generator Configuration 1.0//EN" "http://javacoo.com/dtd/junit-generator-config_1_0.dtd" >
节点说明:
<!-- generatorConfiguration:配置根节点,必须,整个配置文件唯一,定义生成单元测试相关配置 --> <generatorConfiguration> <!-- context:配置上下文节点,必须,可多个,定义生成所需要的模板信息和目标类信息。 属性说明: id,配置上下文的唯一标识,必须,用于在插件配置中指定要执行的上下文节点 --> <context id="TestContext"> <!--template: 模板配置信息,非必须,context唯一,默认使用插件自带模板 属性说明: templatePath:模板路径信息,非必填,相对测试工程目录的模板路径信息,如果填写,则在指定的模板路径查找模板。 templateName:模板名称,非必填,生成单元测试的模板文件名称。 templateHandlerName:模板处理器名称,非必填,默认采用插件自带处理器:default 插件自带处理器说明: default:基于JUnit4的默认的处理器,用于生成普通类(非Spring项目)的单元测试。 spring:基于JUnit4的用于生成Spring工程,相关接口的单元测试。 defaultJUnit5:基于JUnit5的默认的处理器,用于生成普通类(非Spring项目)的单元测试。 springJUnit5:基于JUnit5的用于生成Spring工程,相关接口的单元测试。 mock:基于mockito生成相关接口的单元测试。 --> <template templatePath="/template" templateName="test.ftl" templateHandlerName="spring"></template> <!--classList: 目标类集合节点,必须,context唯一,定义了需要生成单元测试的目标类信息 --> <classList> <!--class: 目标类定义,必须,定义了需要生成单元测试的目标类信息 属性说明: className:类名称,必须,目标全类名。 --> <class className="com.javacoo.junit.generator.api.TestApi"/> </classList> </context> </generatorConfiguration>
-
插件自带模板处理器生成说明:
- 基于JUnit4->default:基于JUnit4的默认的处理器,生成普通类(非Spring项目)的单元测试,只生成了类或者接口的公共方法的单元测试,如:
@Test public void testAddAndGet(){ //TODO: 检查生成的测试代码, 修改给定的方法调用参数 并 断言子句 //准备参数并 调用测试方法 long l = 0L; AtomicLong atomicLong = new AtomicLong(l); long l1 = 0L; long actualResult = atomicLong.addAndGet(l1); assertEquals("addAndGet方法", 0L, actualResult); }
- 基于JUnit4->spring:基于JUnit4的用于生成Spring工程,相关接口的单元测试,只生成了类或者接口的公共方法的单元测试,如:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = { "classpath*:/spring/spring-mvc.xml" }) public class SpringTestApiTest { @Autowired private SpringTestApi springTestApi; @BeforeClass public static void setUpClass(){ //执行所有测试前的操作 } @AfterClass public static void tearDownClass(){ //执行完所有测试后的操作 } @Before public void setUp(){ //每次测试前的操作 } @After public void tearDown(){ //每次测试后的操作 } @Test public void testMyTest3(){ //TODO: 检查生成的测试代码, 修改给定的方法调用参数和断言子句 //准备参数并 调用测试方法 String str = "hello"; String channelNo = "hello"; StoreAreaRequest storeAreaRequest = new StoreAreaRequest(channelNo); List<com.javacoo.junit.generator.model.StoreAreaRequest> storeAreaRequests = new ArrayList<>(); storeAreaRequests.add(storeAreaRequest); String str1 = "hello"; String channelNo1 = "hello"; StoreAreaRequest storeAreaRequest1 = new StoreAreaRequest(channelNo1); Map<java.lang.String, com.javacoo.junit.generator.model.StoreAreaRequest> storeAreaRequestMap = new HashMap<>(); storeAreaRequestMap.put(str1, storeAreaRequest1); String isFaceCheck = "hello"; BigDecimal approveAmt = BigDecimal.ZERO; FundLoanApproveRequest fundLoanApproveRequest = new FundLoanApproveRequest(approveAmt); FundLoanApproveDetailRequest fundLoanApproveDetailRequest = new FundLoanApproveDetailRequest(storeAreaRequests, storeAreaRequestMap, isFaceCheck, fundLoanApproveRequest); Map<java.lang.String, com.javacoo.junit.generator.model.FundLoanApproveDetailRequest> map = new HashMap<>(); map.put(str, fundLoanApproveDetailRequest); ApprovePreQueryResponse actualResult = springTestApi.myTest3(map); assertNotNull(actualResult); } ... }
最佳实践
- 关闭覆盖功能,如:<overwrite>false</overwrite>
- 开启备份功能,如:<backup>true</backup>
- 配置需要执行的上下文ID,如:<contexts>testContext,springTestContext</contexts>
- 在junitGeneratorConfig.xml中定义自己的模块的执行的上下文ID,与其他开发人员隔离
...
插件开发
-
开发步骤
- 实现接口:com.javacoo.junit.generator.api.TemplatePlugin,接口定义如下:
package com.javacoo.junit.generator.api.plugin; import java.util.Map; import com.javacoo.xkernel.spi.Spi; /** * 模板插件 * <li>此插件目的是为自定义模板生成规则提供入口,程序会根据插件提供的模板数据渲染指定路径下,指定模板名称的模板,并输出到指定目录</li> * <li>插件机制基于Java SPI机制的扩展,原理及开发步骤见:https://gitee.com/javacoo/xkernel</li> * <li>注意:目前只支持Freemarker模板引擎,开发手册见:http://freemarker.foofun.cn/</li> * @author: duanyong@jccfc.com * @since: 2021/1/4 10:07 */ @Spi("default") public interface TemplatePlugin { /** * 根据类对象获取模板数据 * <li>此数据用于填充模板</li> * @author duanyong@jccfc.com * @date 2021/1/4 11:09 * @param sourceClass:类对象 * @return: java.util.Map<java.lang.String,java.lang.Object> */ Map<String, Object> getTemplateData(Class sourceClass); /** * 根据类对象获取输出文件路径 * <li>指定测试类文件生成的路径</li> * @author duanyong@jccfc.com * @date 2021/1/4 11:51 * @param sourceClass: 类对象 * @param outputFilePath: 输出路径 * @return: java.lang.String */ String getOutFile(Class sourceClass,String outputFilePath); /** * 获取模板路径 * <li>外部模板所在路径</li> * @author duanyong@jccfc.com * @date 2021/1/8 10:57 * @return: java.lang.String */ String getTemplatePath(); /** * 获取模板名称 * <li>模板名称,带后缀</li> * @author duanyong@jccfc.com * @date 2021/1/5 11:41 * @return: java.lang.String 模板名称 */ String getTemplateName(); }
基于JUnit默认实现类代码片段如下:
AbstractTemplatePlugin
package com.javacoo.junit.generator.internal.plugin.junit4; ... /** * 模板插件接口抽象实现类 * <li>定义了插件所需公共方法及流程</li> * * @author: duanyong@jccfc.com * @since: 2021/1/5 14:27 */ public abstract class AbstractTemplatePlugin implements TemplatePlugin { /**默认模板路径*/ protected static final String BASE_TEMPLATE_PACKAGE = "/templates/"; /**日期格式*/ private final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss"; /**返回变量名*/ private final String RESULT_VAL_NAME = "actualResult"; /** * 根据类对象获取输出文件路径 * <li></li> * * @param sourceClass : 类对象 * @param outputFilePath: 输出路径 * @author duanyong@jccfc.com * @date 2021/1/4 11:51 * @return: java.lang.String */ @Override public String getOutFile(Class sourceClass,String outputFilePath) { Package sourcePackage = sourceClass.getPackage(); //包路径 StringBuilder packagePath = new StringBuilder().append(outputFilePath).append("/").append(sourcePackage.getName().replace(".","/")).append("/"); //生成文件夹 File filePath = new File(packagePath.toString()); if (!filePath.exists()){ filePath.mkdirs(); } //文件名称 String fileName = new StringBuilder().append(sourceClass.getSimpleName().substring(0, 1).toUpperCase()).append(sourceClass.getSimpleName().substring(1)).toString(); //输出文件路径 StringBuilder outFile = packagePath.append(fileName).append("Test.java"); return outFile.toString(); } /** * 是否需要定义测试类变量 * <li></li> * @author duanyong@jccfc.com * @date 2021/1/7 17:18 * @return: boolean */ protected boolean needDefineVal(){ return true; } /** * 构建模板公共数据Map对象 * <li></li> * @author duanyong@jccfc.com * @date 2021/1/7 13:54 * @param sourceClass: 目标class对象 * @return: java.util.Map<java.lang.String,java.lang.Object> */ protected Map<String, Object> buildCommonDataMap(Class sourceClass) { // 定义模板数据 Map<String, Object> data = new HashMap<>(6); //组装基础数据到模板数据Map对象 populateBaseData(sourceClass, data); //组装方法数据到模板数据Map对象 populateMethodMetaData(sourceClass, data); return data; } ... }
DefaultJUnit4TemplatePlugin:
```java
package com.javacoo.junit.generator.internal.plugin.junit4;
import java.util.Map;
import com.javacoo.junit.generator.enmus.JUnitVersionEnum;
import com.javacoo.junit.generator.enmus.TemplateTypeEnum;
/**
* JUnit4模板插件默认实现
* <li></li>
*
* @author: duanyong@jccfc.com
* @since: 2021/1/4 11:18
*/
public class DefaultJUnit4TemplatePlugin extends AbstractTemplatePlugin {
/**
* 根据类对象获取模板数据
* <li></li>
*
* @param sourceClass :类对象
* @author duanyong@jccfc.com
* @date 2021/1/4 11:09
* @return: java.util.Map<java.lang.String, java.lang.Object>
*/
@Override
public Map<String, Object> getTemplateData(Class sourceClass) {
Map<String, Object> data = buildCommonDataMap(sourceClass);
return data;
}
/**
* 获取模板路径
* <li></li>
*
* @author duanyong@jccfc.com
* @date 2021/1/8 10:57
* @return: java.lang.String
*/
@Override
public String getTemplatePath() {
return BASE_TEMPLATE_PACKAGE+ JUnitVersionEnum.JUNIT4.getCode();
}
/**
* 获取模板名称
* <li></li>
*
* @author duanyong@jccfc.com
* @date 2021/1/5 11:41
* @return: java.lang.String 模板名称
*/
@Override
public String getTemplateName() {
return TemplateTypeEnum.TEMPLATE_TYPE_ENUM_DEFAULT.getValue();
}
}
```
-
编写模板文件,如:基于JUnit4的普通类测试模板文件:DefaultTemplate.ftl
package ${basePackage}; import org.junit.*; import static org.junit.Assert.*; <#list importClasses as importClass> import ${importClass}; </#list> /** * ${className}的测试类 * * @author ${author!''} * @date ${date} */ public class ${className}Test { @BeforeClass public static void setUpClass(){ //执行所有测试前的操作 } @AfterClass public static void tearDownClass(){ //执行完所有测试后的操作 } @Before public void setUp(){ //每次测试前的操作 } @After public void tearDown(){ //每次测试后的操作 } <#list methods as method> @Test public void test${method.methodName?cap_first}(){ ${method.methodBody!''} } </#list> }
-
注册接口:在项目resources目录下创建:META-INF/ext目录,并创建一个文本文件:名称为接口的“全限定名”,内容格式为:实现名=实现类的全限定名,如。文件名为:com.javacoo.junit.generator.api.TemplatePlugin。内容如下:
myTemplateHander=com.xxx.plugin.MyJUnit4TemplateHanderPlugin
格式为:处理器名称=处理器实现类全路径类名
3.png
- 使用:在junitGeneratorConfig.xml配置文件的template节点,配置属性 templateHandlerName="myTemplateHander"
future
- 基于JUnit5的单元测试
参数化单元测试 支持Mock 默认mockito实现
项目地址:https://gitee.com/javacoo/junit-generator
一些信息
路漫漫其修远兮,吾将上下而求索
码云:https://gitee.com/javacoo
QQ群:164863067
作者/微信:javacoo
邮箱:xihuady@126.com