Java单元测试实践-00.目录(9万多字文档+700多测试示例)
https://blog.csdn.net/a82514921/article/details/107969340
1. Gradle资源文件与配置参数动态替换
1.1. main模块与test模块资源文件
Gradle任务processResources、processTestResources分别用于复制main模块与test模块的资源文件。
1.1.1. 输入输出文件
在processResources、processTestResources任务中,可以通过getInputs()/getOutputs()方法获取输入输出对应的文件信息。
示例如下:
processResources {
println "inputs: " + getInputs().getFiles().asList()
println "outputs: " + getOutputs().getFiles().asList()
}
用于复制main模块资源文件的processResources任务打印结果如下所示,可以看到输入文件为main模块resources目录的全部文件(Gradle脚本中未指定排除特定文件),输出目录为“build\resources\main”。
inputs: [E:\UnitTest\src\main\resources\applicationContext.xml, E:\UnitTest\src\main\resources\base.properties, E:\UnitTest\src\main\resources\com\adrninistrator\dao\sqlmap\TestTable2Mapper.xml, E:\UnitTest\src\main\resources\com\adrninistrator\dao\sqlmap\TestTableMapper.xml, E:\UnitTest\src\main\resources\log4j2.xml]
outputs: [E:\UnitTest\build\resources\main]
用于复制test模块资源文件的processTestResources任务打印结果如下所示,可以看到输入文件为main模块resources目录的全部文件(Gradle脚本中未指定排除特定文件),输出目录为“build\resources\test”。
inputs: [E:\UnitTest\src\test\resources\applicationContext.xml, E:\UnitTest\src\test\resources\applicationContext_replace.xml, E:\UnitTest\src\test\resources\base.properties, E:\UnitTest\src\test\resources\base_replace.properties, E:\UnitTest\src\test\resources\entityGen\jpa_modify_type.properties, E:\UnitTest\src\test\resources\entityGenConfig\entityGenConfig.yml, E:\UnitTest\src\test\resources\log4j2.xml, E:\UnitTest\src\test\resources\springhibernate\springhibernate.xml, E:\UnitTest\src\test\resources\sql\create_table.sql, E:\UnitTest\src\test\resources\sql\set_schema.sql, E:\UnitTest\src\test\resources\unit_test_config.groovy]
outputs: [E:\UnitTest\build\resources\test]
以上代码也可添加到compileJava、compileTestJava任务中,用于查看main模块与test模块编译产生的类文件的输出目录,默认情况下分别为“build\classes\java\main”“build\classes\java\test”;若项目使用了Lombok等插件,main模块与test模块类的输出目录还会分别增加“build\generated\sources\annotationProcessor\java\ma
in”“build\generated\sources\annotationProcessor\java\te
st”。
1.1.2. Gradle test任务执行时使用的资源文件
使用Gradle test任务执行测试类时,会优先使用test模块的资源文件,对于main模块与test模块均存在的同名资源文件,会使用test模块中的资源文件;对于仅在main模块中存在的同名资源文件,会使用main模块中的资源文件。
- 查看GradleWorkerMain进程的classpath系统属性
使用jinfo命令查看GradleWorkerMain进程的java.class.path系统属性,main模块与test模块的相关目录如下所示。
build\classes\java\test
build\resources\test
build\classes\java\main
build\resources\main
可以看到test模块的类目录build\classes\java\test在main模块的类目录build\classes\java\main之前,test模块的资源文件目录build\resources\test在main模块的资源文件目录build\resources\main之前。
- 在测试类中获取资源文件路径
applicationContext.xml文件在main模块与test模块的resources目录中均存在,在TestConf类中打印classpath中的applicationContext.xml文件路径,可证明使用了test模块中的资源文件,例如/E:/UnitTest/build/resources/test/applicationContext.xml”。
TestTableMapper.xml文件仅在main模块的resources目录中存在,在TestConf类中打印classpath中的TestTableMapper.xml文件路径,证明使用了main模块中的资源文件,例如“/E:/UnitTest/build/resources/main/com/adrninistrator/dao/sqlmap/TestTableMapper.xml”。
1.1.3. test模块资源文件设置
由于Gradle test任务执行时,优先使用test模块中的资源文件,可将main模块中的资源文件拷贝至test模块资源目录中并修改,使Gradle在执行测试类时使用test模块中的资源文件。
- 使用与main模块不同的配置参数
test模块中的资源文件可以使用与main模块不同的配置参数,例如访问不同的依赖环境、使用不同的日志打印参数等。
- 提高测试效率
test模块中的资源文件可将线程池大小调小,或将与测试无关的定时任务禁用,减少测试时的性能与时间开销。
1.2. Gradle脚本动态替换配置参数
1.2.1. 适用场景
动态替换配置参数的功能,可在以下场景使用(包括但不限于以下场景)。
- 生成发布包时修改配置参数
生成不同环境使用的发布包时,可将配置参数替换为对应环境的参数。例如区分生产环境与测试环境。
- 执行测试时修改配置参数
执行测试时,可将配置参数替换为对应环境的参数。例如在本地开发环境执行测试时,访问MySQL数据库;在CI服务器执行测试时,访问本机的H2数据库。
以下方法支持对main模块与test模块的文件内容进行动态替换。
1.2.2. 在filter中使用ReplaceTokens
- filter()方法
参考Gradle对于替换文件内容的说明,“Filtering file content (token substitution, templating, etc.)”( https://docs.gradle.org/current/userguide/working_with_files.html#sec:filtering_files )。
使用文件内容过滤器,可以在复制文件时替换文件的内容。
filter()方法有两个变种,它们的行为不同:
一个使用FilterReader并与Ant过滤器(例如ReplaceTokens)一起使用;
另一个使用闭包或Transformer定义源文件每一行的转换。
请注意,以上两个变种都假定源文件是基于文本的。将replaceTokens类与filter()一起使用时,结果是模板引擎用定义的值替换@tokenName@形式的标记(Ant样式的标记)。
以上Gradle文档对于提供的示例如下:
filter(ReplaceTokens, tokens: [copyright: '2009', version: '2.3.1'])
- ReplaceTokens类
参考ReplaceTokens类的API文档( https://ant.apache.org/manual/api/org/apache/tools/ant/filters/ReplaceTokens.html ),可以看到ReplaceTokens类间接继承自FilterReader类,因此可在filter()方法中使用。
查看Ant的org.apache.tools.ant.filters.ReplaceTokens类源码( https://github.com/apache/ant/blob/master/src/main/org/apache/tools/ant/filters/ReplaceTokens.java )。
其中包含setTokens()方法,参数为Hashtable<String, String> hash,该方法用于设置需要替换的标记Map,参数hash的key对应需要替换的标记,value为需要替换的值,参数hash不能为null。
根据以上内容可知,Gradle脚本filter()方法中的tokens参数用于设置ReplaceTokens类的属性,tokens属性支持Hashtable<String, String>类型。在Gradle脚本的filter()方法中使用ReplaceTokens类时,tokens参数也可以使用字符串形式"tokens",更容易理解。
1.2.3. 使用ConfigSlurper读取Groovy配置文件
上述Gradle文档中提供的替换文档的示例中,需要替换的标记及替换后的值,均声明在Gradle脚本中,也支持从配置文件读取。
- ConfigSlurper类
参考Groovy的ConfigSlurper类( https://docs.groovy-lang.org/latest/html/documentation/#_configslurper )。
ConfigSlurper是一个工具类,可用于读取Groovy脚本形式的配置文件。Groovy配置文件与Java使用的.properties文件类似,ConfigSlurper允许使用点符号和闭包范围的配置值以及任意对象类型。
ConfigSlurper类的parse()方法可用于返回groovy.util.ConfigObject实例。ConfigObject是特殊的java.util.Map实现,它返回配置的值或新的ConfigObject实例,不可能为null。
Groovy配置文件示例如下,属性environments.development.app.port值为8080。
environments {
development {
app.port = 8080
}
test {
app.port = 8082
}
production {
app.port = 80
}
}
在创建ConfigSlurper对象时,指定所需的第二级元素名称,调用parse()方法可获取到第二级元素下的元素。
例如对应上述Groovy配置文件,使用new ConfigSlurper(‘development’).parse()方法,可以获取到environments.development元素的值,即app.port = 8080。
"environments"是内置的,使用registerConditionalBlock()方法可以注册其他的方法名称,该方法的参数为“String blockName, String blockValue”,应分别指定第一级元素名称,与所需的第二级元素名称。示例如下:
def slurper = new ConfigSlurper()
slurper.registerConditionalBlock('environments', 'development')
参考ConfigSlurper类的API文档( http://docs.groovy-lang.org/latest/html/gapi/groovy/util/ConfigSlurper.html ),可以看到存在参数为“URL scriptLocation”的parse()方法,可指定File对象toURI().toURL()返回的URL对象,用于从指定的文件读取配置信息。
- ConfigObject类
参考 https://docs.groovy-lang.org/latest/html/documentation/#_configslurper 。
在与Java集成时,可以使用toProperties()方法将ConfigObject转换为java.util.Properties对象。请注意,在将配置值添加到新创建的Properties实例时会将其转换为String实例。
因此ConfigObject对象的toProperties()方法返回值,可在Gradle脚本的filter()方法作为ReplaceTokens类的tokens参数使用,即可以通过Groovy配置文件指定文件内容替换的相关数据。
参考groovy.util.ConfigObject类的API文档( http://docs.groovy-lang.org/latest/html/gapi/groovy/util/ConfigObject.html )。
使用ConfigObject类的toProperties()方法,可以转换为java.util.Properties格式,会预先展平树结构。
假如有以下Groovy配置文件:
a {
b {
c1 {
d1 = 'string'
d2 = 123
d3 = 1.23
d4 = true
}
c3 = 1.23
c4 = true
}
}
使用以下代码读取上述Groovy配置文件的内容,获取ConfigObject对象后,调用toProperties()方法获取Properties对象。
Properties properties = new ConfigSlurper().parse(file("xxx.groovy").toURI().toURL()).toProperties()
for (Map.Entry entry : properties.entrySet()) {
println(entry.getKey() + " *" + entry.getValue() + "* " + entry.getValue().getClass().getSimpleName())
}
执行结果如下:
a.b.c3 *[true, 123, string]* String
a.b.c2 *1.23* String
a.b.c1.d4 *true* String
a.b.c1.d3 *1.23* String
a.b.c1.d2 *123* String
a.b.c1.d1 *string* String
可以看到生成的Properties对象的key为Groovy配置文件中的数据的层级关系,每一级元素之间使用“.”分隔,value类型均为String。
对ConfigSlurper对象使用registerConditionalBlock()方法指定需要获取数据的名称,如下所示:
ConfigSlurper configSlurper = new ConfigSlurper()
configSlurper.registerConditionalBlock("a", "b")
执行结果如下:
c1.d4 *true* String
c3 *[true, 123, string]* String
c1.d3 *1.23* String
c2 *1.23* String
c1.d2 *123* String
c1.d1 *string* String
可以看到获取的Properties对象的key为a.b之下层级的元素。
1.2.4. 使用自定义标识符
使用ReplaceTokens类对文件内容进行替换时,默认使用“@”作为替换标记,待替换的原始数据格式为“@tokenName@”。例如待替换内容配置文件中包含“@a.b.c@”,Groovy配置文件中包含参数a.b.c=value,则执行替换操作后,被替换内容的配置文件中,“@tokenName@”会变成“value”。
查看ReplaceTokens类的API文档( https://ant.apache.org/manual/api/org/apache/tools/ant/filters/ReplaceTokens.html ),可以看到包含setBeginToken()、setEndToken()方法,分别用于指定开头及结尾的替换标志。
假如ReplaceTokens类默认的替换标志“@tokenName@”不满足使用要求时,可以通过在Gradle脚本的filter()方法指定ReplaceTokens类的beginToken与endToken属性。如下所示,将替换标志设置为“@@tokenName##”的形式。
filter(org.apache.tools.ant.filters.ReplaceTokens,
"tokens": configObject.toProperties(),
"beginToken": "@@",
"endToken": "##")
1.2.5. 配置文件中文乱码问题
在Gradle脚本的filter()方法使用ReplaceTokens类对文件内容进行替换时,编码为UTF-8的文件中的中文内容在替换后会出现乱码,使Gradle执行时使用UTF-8编码,可解决该问题:
当使用Gradle Wrapper时,在项目中的gradlew.bat/gradlew文件中,在DEFAULT_JVM_OPTS参数增加"-Dfile.encoding=UTF-8";
当不使用Gradle Wrapper时,在Gradle安装目录的bin/gradlew.bat/gradlew文件中,在DEFAULT_JVM_OPTS参数增加"-Dfile.encoding=UTF-8"。
进行以上配置后,在Windows环境执行Gradle命令时,输出的中文会出现乱码(例如编译过程中的提示信息),这是因为Windows批处理窗口的默认编码不是UTF-8。
可在Windows批处理窗口执行“chcp 65001”命令,将编码设置为UTF-8,并将字体修改为True Type字体"Lucida Console",大部分中文不再乱码,但会出现重复。
1.2.6. 示例项目配置文件使用
在示例项目中,根据不同的测试模式(测试验证的内容不相同),需要使用不同的参数配置,在Gradle脚本中根据代表测试模式的参数,对资源文件内容进行替换,如下所示。
- 根据JVM参数决定使用的参数
在unittest.gradle脚本的ext块中,使用执行Gradle任务时JVM参数中的“testMode”参数,作为当前测试模式,决定当前使用的配置参数。
当未指定该参数时,使用默认测试模式“default_mode”。
在执行Gradle任务时指定“testMode”参数示例如下:
gradlew test -DtestMode=fast
- Groovy配置文件
在src/test/resources/unit_test_config.groovy配置文件中,配置了不同的测试模式对应的配置参数,第一级元素名称为“test_mode”,第二级元素代表不同的测试模式,如下所示:
test_mode{
xxx {
jdbc {
driver = 数据库驱动名称
url = 数据库地址
username = 数据库用户名
password = 数据库密码
}
import_jpa = 引入JPA配置
test_include = 测试类包含范围(数组)
test_exclude = 测试类排除范围(数组)
jacoco_include = 生成代码覆盖率的类包含范围 ( 数组 )
jacoco_exclude = 生成代码覆盖率的类排除范围 ( 数组 )
}
}
- 查看示例支持的testMode参数
unittest.gradle脚本中包含showTestMode任务,用于显示支持的测试模式。
- 读取Groovy配置文件
在unittest.gradle脚本的ext块中,对Groovy配置文件进行了读取,如下所示:
ext {
testConfigFilePath = "src/test/resources/unit_test_config.groovy"
configSlurper = new ConfigSlurper()
configSlurper.registerConditionalBlock("test_mode", testMode)
configObject = configSlurper.parse(file(testConfigFilePath).toURI().toURL())
}
- 替换配置文件内容
使用Gradle的test任务执行示例工程单元测试时,需要在使用不同的测试模式时,替换应用程序使用的applicationContext.xml、base.properties配置文件中的参数值。
若以上配置文件中包含替换文件内容时使用的标记,在通过IDE执行单元测试时,替换文件内容的操作不会执行,会导致应用程序无法正确读取配置文件。
因此新增配置文件applicationContext_replace.xml、base_replace.properties,在以上文件中指定替换文件内容时使用的标记,如下所示:
jdbc.driver=@jdbc.driver@
jdbc.url=@jdbc.url@
jdbc.username=@jdbc.username@
jdbc.password=@jdbc.password@
在unittest.gradle脚本的processTestResources任务中,在复制test模块资源文件“applicationContext_replace.xml”与“base_replace.properties”时,在filter()方法中指定通过ReplaceTokens类对文件内容进行替换,使用ext块中获取的configObject变量作为“tokens”参数,如下所示:
processTestResources {
from(sourceSets.test.resources) {
include "applicationContext_replace.xml", "base_replace.properties"
filter(org.apache.tools.ant.filters.ReplaceTokens, "tokens": configObject.toProperties())
}
}
filter()方法支持在from()方法中使用,或直接在processTestResources等任务中使用。
在from()方法中,也可不使用include指定需要替换内容的文件范围,即对test模块的全部资源文件均尝试进行替换(内容不含标识的文件不会被替换)。
sourceSets.test.resources也可使用sourceSets.test.resources.srcDirs。
- 覆盖配置文件
在对配置文件applicationContext_replace.xml、base_replace.properties的文件内容进行替换后,需要覆盖应用程序使用的对应配置文件applicationContext.xml、base.properties,在processTestResources任务的doLast Action中,执行替换操作,如下所示:
processTestResources {
doLast {
file('build/resources/test/base.properties').renameTo(file('build/resources/test/base.properties.bak'))
file('build/resources/test/base_replace.properties').renameTo(file('build/resources/test/base.properties'))
file('build/resources/test/applicationContext.xml').renameTo(file('build/resources/test/applicationContext.xml.bak'))
file('build/resources/test/applicationContext_replace.xml').renameTo(file('build/resources/test/applicationContext.xml'))
}
}
1.2.7. 获取Groovy配置文件参数值
使用ConfigSlurper类读取Groovy配置文件中的数据时,获得的ConfigObject对象中包含了原始类型的数据(使用toProperties()方法时参数值类型会变为String)。
假如存在以下Groovy配置文件:
a {
b {
c1 {
d1 = 'string\\1\'2'
d2 = 123
d3 = 1.23
d4 = true
}
c2 = 1.23
c3 = [true, 123, 'string']
}
}
可通过以下代码进行读取:
ConfigObject configObject = new ConfigSlurper().parse(file("src/test/resources/groovyConfig/test.groovy").toURI().toURL())
ConfigObject configObjectA = configObject.get("a")
println "a: " + configObjectA + " " + configObjectA.getClass().getSimpleName()
ConfigObject configObjectB = configObjectA.get("b")
println "b: " + configObjectB + " " + configObjectB.getClass().getSimpleName()
ConfigObject configObjectC1 = configObjectB.get("c1")
println "c1: " + configObjectC1.getClass().getSimpleName()
for (Map.Entry entry : configObjectC1.entrySet()) {
println "c1: " + entry.getKey() + " " + entry.getValue() + " " + entry.getValue().getClass().getSimpleName()
}
def c2 = configObjectB.get("c2")
println "c2: " + c2 + " " + c2.getClass().getSimpleName()
List c3 = configObjectB.get("c3")
println "c3: " + c3 + " " + c3.getClass().getSimpleName()
for (Object o : c3) {
println "c3: " + o + " " + o.getClass().getSimpleName()
}
执行结果如下所示,可以看到从Groovy配置文件读取的数据保持了原有格式,ConfigObject为嵌套格式,参数值支持String、Integer、BigDecimal、Boolean等类型,支持数组格式。
a: [b:[c1:[d1:string\1'2, d2:123, d3:1.23, d4:true], c2:1.23, c3:[true, 123, string]]] ConfigObject
b: [c1:[d1:string\1'2, d2:123, d3:1.23, d4:true], c2:1.23, c3:[true, 123, string]] ConfigObject
c1: ConfigObject
c1: d1 string\1'2 String
c1: d2 123 Integer
c1: d3 1.23 BigDecimal
c1: d4 true Boolean
c2: 1.23 BigDecimal
c3: [true, 123, string] ArrayList
c3: true Boolean
c3: 123 Integer
c3: string String
1.2.7.1. Groovy特殊字符转义
根据以上现象可知,Groovy配置文件中的“\”“’”需要进行转义。
参考 https://docs.groovy-lang.org/latest/html/documentation/#_escaping_special_characters 。
可以使用反斜杠字符“\”对单引号进行转义,以避免终止字符串文字:
'an escaped single quote: \' needs a backslash'
也可以用双反斜杠对转义字符进行转义:
'an escaped escape character: \\ needs a double backslash'
以下为使用反斜杠作为转义字符的特殊字符:
字符 | 说明 |
---|---|
\t | 制表符tabulation |
\b | 退格键backspace |
\n | 换行 |
\r | 回车 |
\f | 换页 |
\\ | 反斜杠 |
\’ | 单引号字符串中的单引号(对于三重单引号和双引号字符串是可选的) |
\" | 双引号字符串中的双引号(三重双引号和单引号字符串是可选的) |
1.2.8. 测试范围设置
若执行测试时需要根据参数区分测试范围时,可将测试范围保存在Groovy配置文件中,与Gradle脚本分离。
test任务中的filter()方法中的setIncludePatterns()/setExcludePatterns()方法参数不支持数组或集合形式,因此不使用。可使用setIncludes()/setExcludes()方法指定需要包含/排除的测试范围,与test任务中的include或exclude参数使用方法类似。
在示例项目中通过以上方式设置测试范围。
在Groovy配置文件中unit_test_config.groovy,配置了测试范围参数,如下所示:
test_include = ['**']
test_exclude = ['adrninistrator/test/testdatabase/**', 'adrninistrator/test/testmock/mybatis/**', '**/TestSuite**']
在unittest.gradle脚本的test任务的doFirst Action中,对测试范围进行了设置,如下所示:
def testInclude = configObject.get("test_include")
if (testInclude != null) {
setIncludes(testInclude)
}
def testExclude = configObject.get("test_exclude")
if (testExclude != null) {
setExcludes(testExclude)
}
1.3. 根据Gradle执行的任务改变操作
在某些情况下,需要根据Gradle执行的任务改变操作。例如当Gradle执行的任务包含a时,执行操作1;任务不包含a时,执行操作2。
通过以下代码可以获得Gradle当前执行的任务,可参考示例项目unittest.gradle文件中的gradleArgsContains()方法:
boolean gradleArgsContains(String... expectedArgs) {
StartParameter startParameter = gradle.getStartParameter()
if (startParameter.isDryRun()) {
return false
}
List<TaskExecutionRequest> taskExecutionRequestList = startParameter.getTaskRequests()
for (TaskExecutionRequest taskExecutionRequest : taskExecutionRequestList) {
List<String> args = taskExecutionRequest.getArgs()
for (String arg : args) {
for (String expectedArg : expectedArgs) {
if (arg.equals(expectedArg)) {
return true
}
}
}
}
return false
}
以上使用的方法可参考 https://docs.gradle.org/current/dsl/org.gradle.api.tasks.GradleBuild.html#org.gradle.api.tasks.GradleBuild:startParameter 、 https://docs.gradle.org/current/javadoc/org/gradle/StartParameter.html#getTaskRequests-- 、 https://docs.gradle.org/current/javadoc/org/gradle/TaskExecutionRequest.html#getArgs-- 。
示例项目在以下任务中根据Gradle执行的任务改变操作,避免多余的操作:
- processTestResources任务
在processTestResources任务中,使用gradleArgsContains()方法判断当执行的Gradle任务包含test任务时,才执行替换配置文件中的测试参数等操作。
- jacocoTestReport任务
在jacocoTestReport任务中,使用gradleArgsContains()方法判断当执行的Gradle任务包含当前任务时,才执行设置需要生成覆盖率的代码范围操作。
参考 https://docs.gradle.org/current/javadoc/org/gradle/api/Task.html#getName-- ,可以使用getName()方法获取当前任务的名称。
在其他情况下,也可以判断Gradle执行的任务是否包含指定任务,优化Gradle任务的操作。
1.3.1. 解决processResources任务导致找不到资源文件的问题
某些情况下,在生成项目发布使用的jar包或war包时,只需要包含部分资源文件,可能会在processResources任务中通过include指定需要包含的资源文件,如下所示:
processResources {
include 'com/**'
}
当进行以上配置后,在IDEA中执行单元测试,或正常的Java类时(可能需要清理out目录),以上配置也会生效,导致找不到所需的资源文件。
例如示例项目的main模块包含以下资源文件:
执行MainServer类的main()方法,提示找不到applicationContext.xml文件,查看out/production/resources目录,仅包含以上配置指定的com目录中的资源文件,不包含其他资源文件。
对processResources任务进行修改,判断Gradle执行的任务包含build、jar、war等任务时才执行include操作,如下所示:
processResources {
if (gradleArgsContains("build", "jar", "war")) {
include 'com/**'
}
}
经过以上配置后,可以解决在IDEA执行单元测试,或正常的Java类时,由于processResources任务的配置导致找不到资源文件的问题。同时,生成项目发布使用的jar包或war包时,也能指定需要包含的资源文件。
再次执行示例项目的MainServer类,可以正常执行。执行“gradlew/gradle jar”命令生成jar包,查看其中包含的资源文件只有com目录中的,证明以上配置可以满足不同情况的需求。