输入输出无依赖型函数的GroovySpock单测模板的自动生成工具(上)
目标###
在《使用Groovy+Spock轻松写出更简洁的单测》 一文中,讲解了如何使用 Groovy + Spock 写出简洁易懂的单测。 对于相对简单的无外部服务依赖型函数,通常可以使用 expect-where 的形式。
本文尝试自动生成无外部服务依赖型函数的Spock单测模板,减少编写大量单测的重复工作量,只需要构造相应的测试数据集即可。
分析与思路###
首先,需要仔细分析下无外部服务依赖型函数的Spock单测模板的组成。 比如
class BinarySearchTest extends Specification {
def "testSearch"() {
expect:
BinarySearch.search(arr as int[], key) == result
where:
arr | key | result
[] | 1 | -1
[1] | 1 | 0
[1] | 2 | -1
[3] | 2 | -1
[1, 2, 9] | 2 | 1
[1, 2, 9] | 9 | 2
[1, 2, 9] | 3 | -1
//null | 0 | -1
}
}
使用模板替换的思路最为直接。我们将使用Groovy的模板机制。分析这个单测组成,可以得到两个模板:
方法的模板####
method.tpl
@Test
def "test${Method}"() {
expect:
${inst}.invokeMethod("${method}", [${paramListInMethodCall}]) == ${result}
where:
${paramListInDataProvider} | ${result}
${paramValues} | ${resultValue}
}
有几点说明下:
- 之所以用 invokeMethod ,是为了既适应public方法也适应 private 方法,因为只要调用到相应方法返回值即可。当然,这样增加了点击进去查看方法的麻烦程度。可以做个优化。
- 之所以有 Method 和 method 变量,是因为 test${Method} 需要有变化,比如考虑到重载方法,这里就不能生成同名测试方法了。
- paramListInMethodCall,paramListInDataProvider 只有分隔符的区别。通过解析参数类型列表来得到。
类的模板####
spocktest.tpl
package ${packageName}
import org.junit.Test
import spock.lang.Specification
${BizClassNameImports}
/**
* AutoGenerated BY AutoUTGenerator.groovy
*/
class ${ClassName}AutoTest extends Specification {
def $inst = new ${ClassName}()
${AllTestMethods}
}
BizClassNameImports 需要根据对所有方法的解析,得到引入的类列表而生成。
现在,思路很明晰了: 通过对测试类及方法的反射解析,得到相应的信息,填充到模板变量里。
详细设计###
数据结构设计####
接下来,需要进行数据结构设计。尽管我们也能一团面条地解析出所需数据然后填充,那样必然使代码非常难读,而且不易扩展。 我们希望整个过程尽可能明晰,需要增加新特性的时候只需要改动局部。 因此,需要仔细设计好相应的数据结构, 然后将数据结构填充进去。
根据对模板文件的分析,可以知道,我们需要如下两个对象:
class AllInfoForAutoGeneratingUT {
String packageName
String className
List<MethodInfo> methodInfos
}
class MethodInfo {
String methodName
List<String> paramTypes
List<String> classNamesToImported
String returnType
}
算法设计####
接下来,进行算法设计,梳理和写出整个流程。
STEP1: 通过反射机制解析待测试类的包、类、方法、参数信息,填充到上述对象中;
STEP2: 加载指定模板,使用对象里的信息替换模板变量,得到测试内容Content;
STEP3:根据待测试类的路径生成对应测试类的路径和文件TestFile.groovy;
STEP4: 向 TestFile.groovy 写入测试内容 Content;
STEP5: 细节调优。
细节调优###
完整源代码见附录。这里对一些细节的处理做一点说明。
参数名的处理####
一个问题是, 如果方法参数类型里含有 int , long 等基本类型,那么生成的单测里会含有这些关键字,导致单测编译有错。比如,若方法是 X.getByCode(Integer code), 那么生成的测试方法调用是 x.invokeMethod("getByCode", [integer]) , 若方法是 X.getByCode(int code) ,那么生成的测试方法调用是 x.invokeMethod("getByCode", [int]) ,就会因为参数名为关键字 int 而报错。解决办法是,生成参数名时统一加了个 val 后缀; 如果是列表,就统一加个 list 后缀; 见 typeMap。
以小见大: 解决一个问题,不应只局限于解决当前问题,而要致力于发明一种通用机制,解决一类问题。
另一个问题是,如果方法里含有多个相同类型的参数,那么生成 where 子句的头时,就会有重复,导致无法跑 spock 单测。 见如下:
public static List<Creative> query(CreativeQuery query, int page, int size ) {
return new ArrayList();
}
@Test
def "testQuery"() {
expect:
x.invokeMethod("query", [creativeQuery,intval,intval]) == resultlist
where:
creativeQuery0 | intval | intval | resultlist
new CreativeQuery([:]) | 0 | 0 | []
}
解决的办法是,遍历参数列表生成参数名时,最后再加上参数的位置索引,这样就不会重复了。这个方法其实也可以解决上面的问题。 这就是如下代码的作用。
def paramTypeList = []
m.paramTypes.eachWithIndex {
def entry, int ind -> paramTypeList.add(mapType(firstLowerCase(entry), false) + ind)
}
生成的单测是:
@Test
def "testQuery"() {
expect:
x.invokeMethod("query", [creativeQuery0,intval1,intval2]) == resultlist
where:
creativeQuery0 | intval1 | intval2 | resultlist
new CreativeQuery([:]) | 0 | 0 | []
}
同名方法的处理####
如果一个类含有方法重载,即含有多个同名方法,那么生成的测试类方法名称就相同了,导致单测编译有错。由于 groovy 的测试方法名可以是带引号的字符串,因此,这里在生成测试方法名时,就直接带上了参数的类型,避免重复。
"Method": firstUpperCase(m.methodName) + "(" + m.paramTypes.join(",") + ")",
生成的测试方法名是: "testQuery(CreativeQuery,int,int)" 而不是简单的 "testQuery"
测试数据构造####
对于Java语言支持的类型,可以通过 typeDefaultValues 构建一个 类型到 默认值的映射; 对于自定义的类型,怎么办呢 ? 这里,可以通过先构造一个Map,再用工具类转成对应的对象。Groovy 的构造器非常方便,可以直接用 new 类名(map) 来得到对象。
当然,对于含有嵌套对象的对象的构造,还需要优化。
完整源代码###
目录结构####
ProjectRoot
templates/
method.tpl, spocktest.tpl
sub-module
src/main/(java,resource,groovy)
src/test/groovy
autout
AutoUTGenerator.groovy
GroovyUtil.groovy
实际应用中,可以把模板文件放在 src/main/resource 下。
代码####
AutoUTGenerator.groovy
package autout
import groovy.text.SimpleTemplateEngine
import zzz.study.X
import java.lang.reflect.Method
/**
* Created by shuqin on 18/6/22.
*/
class AutoUTGenerator {
def static projectRoot = System.getProperty("user.dir")
static void main(String[] args) {
ut X.class
// ut("com.youzan.ebiz.trade.biz")
}
static void ut(String packageName) {
List<String> className = ClassUtils.getClassName(packageName, true)
className.collect {
ut Class.forName(it)
}
}
/**
* 生成指定类的单测模板文件
*/
static void ut(Class testClass) {
def packageName = testClass.package.name
def className = testClass.simpleName
def methods = testClass.declaredMethods.findAll { ! it.name.contains("lambda") }
def methodInfos = methods.collect { parse(it) }
def allInfo = new AllInfoForAutoGeneratingUT(
packageName: packageName,
className: className,
methodInfos: methodInfos
)
def content = buildUTContent allInfo
def path = getTestFileParentPath(testClass)
def dir = new File(path)
if (!dir.exists()) {
dir.mkdirs()
}
def testFilePath = "${path}/${className}AutoTest.groovy"
writeUTFile(content, testFilePath)
println("Success Generate UT for $testClass.name in $testFilePath")
}
/**
* 解析拿到待测试方法的方法信息便于生成测试方法的内容
*/
static MethodInfo parse(Method m) {
def methodName = m.name
def paramTypes = m.parameterTypes.collect { it.simpleName }
def classNamesToImported = m.parameterTypes.collect { it.name }
def returnType = m.returnType.simpleName
new MethodInfo(methodName: methodName,
paramTypes: paramTypes,
classNamesToImported: classNamesToImported,
returnType: returnType
)
}
/**
* 根据单测模板文件生成待测试类的单测类模板
*/
static buildUTContent(AllInfoForAutoGeneratingUT allInfo) {
def spockTestFile = new File("${projectRoot}/templates/spocktest.tpl")
def methodContents = allInfo.methodInfos.collect { generateTestMethod(it, allInfo.className) }
.join("\n\n")
def engine = new SimpleTemplateEngine()
def imports = allInfo.methodInfos.collect { it.classNamesToImported }
.flatten().toSet()
.findAll { isNeedImport(it) }
.collect { "import " + it } .join("\n")
def binding = [
"packageName": allInfo.packageName,
"ClassName": allInfo.className,
"inst": allInfo.className.toLowerCase(),
"BizClassNameImports": imports,
"AllTestMethods": methodContents
]
def spockTestContent = engine.createTemplate(spockTestFile).make(binding) as String
return spockTestContent
}
static Set<String> basicTypes = new HashSet<>(["int", "long", "char", "byte", "float", "double", "short"])
static boolean isNeedImport(String importStr) {
def notToImport = importStr.startsWith('[') || importStr.contains("java") || (importStr in basicTypes)
return !notToImport
}
/**
* 根据测试方法模板文件 method.tpl 生成测试方法的内容
*/
static generateTestMethod(MethodInfo m, String className) {
def engine = new SimpleTemplateEngine()
def methodTplFile = new File("${projectRoot}/templates/method.tpl")
def paramValues = m.paramTypes.collect { getDefaultValueOfType(firstLowerCase(it)) }.join(" | ")
def returnValue = getDefaultValueOfType(firstLowerCase(m.returnType))
def paramTypeList = []
m.paramTypes.eachWithIndex {
def entry, int ind -> paramTypeList.add(mapType(firstLowerCase(entry), false) + ind)
}
def binding = [
"method": m.methodName,
"Method": firstUpperCase(m.methodName) + "(" + m.paramTypes.join(",") + ")",
"inst": className.toLowerCase(),
"paramListInMethodCall": paramTypeList.join(","),
"paramListInDataProvider": paramTypeList.join(" | "),
"result": mapType(firstLowerCase(m.returnType), true),
"paramValues": paramValues,
"resultValue": returnValue
]
return engine.createTemplate(methodTplFile).make(binding).toString() as String
}
/**
* 写UT文件
*/
static void writeUTFile(String content, String testFilePath) {
def file = new File(testFilePath)
if (!file.exists()) {
file.createNewFile()
}
def printWriter = file.newPrintWriter()
printWriter.write(content)
printWriter.flush()
printWriter.close()
}
/**
* 根据待测试类生成单测类文件的路径(同样的包路径)
*/
static getTestFileParentPath(Class testClass) {
println(testClass.getResource("").toString())
testClass.getResource("").toString() // GET: file:$HOME/Workspace/java/project/submodule/target/classes/packagePath/
.replace('/target/test-classes', '/src/test/groovy')
.replace('/target/classes', '/src/test/groovy')
.replace('file:', '')
}
/** 首字母小写 */
static String firstLowerCase(String s) {
s.getAt(0).toLowerCase() + s.substring(1)
}
/** 首字母大写 */
static String firstUpperCase(String s) {
s.getAt(0).toUpperCase() + s.substring(1)
}
/**
* 生成参数列表中默认类型的映射, 避免参数使用关键字导致跑不起来
*/
static String mapType(String type, boolean isResultType) {
def finalType = typeMap[type] == null ? type : typeMap[type]
(isResultType ? "result" : "") + finalType
}
static String getDefaultValueOfType(String type) {
def customerType = firstUpperCase(type)
typeDefaultValues[type] == null ? "new ${customerType}([:])" : typeDefaultValues[type]
}
def static typeMap = [
"string": "str", "boolean": "bval", "long": "longval", "Integer": "intval",
"float": "fval", "double": "dval", "int": "intval", "object[]": "objectlist",
"int[]": "intlist", "long[]": "longlist", "char[]": "chars",
"byte[]": "bytes", "short[]": "shortlist", "double[]": "dlist", "float[]": "flist"
]
def static typeDefaultValues = [
"string": "\"\"", "boolean": true, "long": 0L, "integer": 0, "int": 0,
"float": 0, "double": 0.0, "list": "[]", "map": "[:]", "date": "new Date()",
"int[]": "[]", "long[]": "[]", "string[]": "[]", "char[]": "[]", "short[]": "[]", "byte[]": "[]", "booloean[]": "[]",
"integer[]": "[]", "object[]": "[]"
]
}
class AllInfoForAutoGeneratingUT {
String packageName
String className
List<MethodInfo> methodInfos
}
class MethodInfo {
String methodName
List<String> paramTypes
List<String> classNamesToImported
String returnType
}
改进点###
- 模板可以更加细化,比如支持异常单测的模板;有参函数和无参函数的模板;模板组合;
- 含嵌套对象的复杂对象的测试数据生成;
- 写成 IntellJ 的插件。
输入输出无依赖型函数的GroovySpock单测模板的自动生成工具(上)的更多相关文章
- 使用Java函数接口及lambda表达式隔离和模拟外部依赖更容易滴单测
概述 单测是提升软件质量的有力手段.然而,由于编程语言上的支持不力,以及一些不好的编程习惯,导致编写单测很困难. 最容易理解最容易编写的单测,莫过于独立函数的单测.所谓独立函数,就是只依赖于传入的参数 ...
- 无依赖简单易用的Dynamics 365公共视图克隆工具
本人微信公众号:微软动态CRM专家罗勇 ,回复279或者20180818可方便获取本文,同时可以在第一间得到我发布的最新博文信息,follow me!我的网站是 www.luoyong.me . Dy ...
- 如何利用pip自动生成和安装requirements.txt依赖
在查看别人的Python项目时,经常会看到一个requirements.txt文件,里面记录了当前程序的所有依赖包及其精确版本号.这个文件有点类似与Rails的Gemfile.其作用是用来在另一台PC ...
- [python] [转]如何自动生成和安装requirements.txt依赖
[转]如何自动生成和安装requirements.txt依赖 在查看别人的Python项目时,经常会看到一个requirements.txt文件,里面记录了当前程序的所有依赖包及其精确版本号.这个文件 ...
- 如何自动生成和安装requirements.txt依赖
在查看别人的Python项目时,经常会看到一个requirements.txt文件,里面记录了当前程序的所有依赖包及其精确版本号.这个文件有点类似与Rails的Gemfile.其作用是用来在另一台PC ...
- 自动生成和安装requirements.txt依赖
在查看别人的Python项目时,经常会看到一个requirements.txt文件,里面记录了当前程序的所有依赖包及其精确版本号.这个文件有点类似与Rails的Gemfile.其作用是用来在另一台PC ...
- 使用Groovy+Spock轻松写出更简洁的单测
当无法避免做一件事时,那就让它变得更简单. 概述 单测是规范的软件开发流程中的必不可少的环节之一.再伟大的程序员也难以避免自己不犯错,不写出有BUG的程序.单测就是用来检测BUG的.Java阵营中,J ...
- 【spock】单测竟然可以如此丝滑
0. 为什么人人都讨厌写单测 在之前的关于swagger文章里提到过,程序员最讨厌的两件事,一件是别人不写文档,另一件就是自己写文档.这里如果把文档换成单元测试也同样成立. 每个开发人员都明白单元测试 ...
- 上传图片,多图上传,预览功能,js原生无依赖
最近很好奇前端的文件上传功能,因为公司要求做一个支持图片预览的图片上传插件,所以自己搜了很多相关的插件,虽然功能很多,但有些地方不能根据公司的想法去修改,而且需要依赖jQuery或Bootstrap库 ...
随机推荐
- 重写toString()
重写Object的toString()之前,得到的结果是 类型 @ 内存地址 demo: package cn.sasa.demo1; public class Test { public stat ...
- CF891C Envy 最小生成树/虚树
正解:最小生成树/虚树 解题报告: 传送门! sd如我就只想到了最暴力的想法,一点儿优化都麻油想到,,,真的菜到爆炸了QAQ 然后就分别港下两个正解QAQ 法一,最小生成树 这个主要是要想到关于最小生 ...
- ES6的十大新特性(转)
add by zhj: 该文章是由国外一哥们写的,由腾讯前端团队翻译,图片中的妹子长得挺好看的,很养眼,嘿嘿.我目前在学习ES6,这篇文章把ES6的 几个主要新特性进行了归纳总结,犹如脑图一般,让人看 ...
- WCF访问超时:HTTP 请求已超过xx:yy分配的超时。为此操作分配的时间可能是较长超时的一部分。
在服务端设置时间长些 <client> <endpoint address="http://43.98.49.189:5700/UPJWCFServcie.svc" ...
- python-面向对象-01_面向对象(OOP)基本概念
面向对象(OOP)基本概念 面向对象编程 —— Object Oriented Programming 简写 OOP 目标 了解 面向对象 基本概念 01. 面向对象基本概念 我们之前学习的编程方式就 ...
- 轻松了解JS中this的指向
JS中的this指向一直是个让人头疼的问题,想当初我学的是天昏地暗,查了好多资料,看的头都大了,跟他大战了那么多回合,终于把它搞定个七八分,其实往往都是我们复杂化了,现在就让大家轻松看懂this的指向 ...
- 矩形嵌套(dp)
矩形嵌套 时间限制:3000 ms | 内存限制:65535 KB 难度:4 描述 有n个矩形,每个矩形可以用a,b来描述,表示长和宽.矩形X(a,b)可以嵌套在矩形Y(c,d)中当且仅当a& ...
- entry.define编程思路
0.lua将文字传给场景脚本. 1.场景脚本将pattern.define文件中的PAT当作子弹(水泡弹,带颜色) 2.用户的问题作为客户端的请求,发送给服务器端 3.服务器端接受客户端的问题请求 4 ...
- [vue]组件篇
slot&子组件通过computed修改父组件数据 <div id="app"> <modal type="primary"> ...
- wordvector to sentence vector
wordvector已经通过word2vec训练出来了,可是如何通过WV得到SV(Sentence Vector)? 思路1: 直接将句子的向量叠加取平均:效果很不好,每个词没有考虑权重,获取的向量会 ...