在 “Groovy元编程简明教程” 一文中,简明地介绍了 Groovy 元编程的特性。 那么,元编程可以应用哪些场合呢?元编程通常可以用来自动生成一些相似的模板代码。
在 “使用Groovy+Spock构建可配置的订单搜索接口测试用例集” 一文中,谈到了如何将搜索接口的测试用例配置化。 不过,那还只是初级配置化, 含有浓浓的 Java 对象味了, 测试代码与测试用例集合的配置实际上并没有分离,整个测试方法看起来不够清晰。 那么,用元编程的方法,会是怎样呢 ?
首先,来看一个简单的例子。 这个例子使用了“闭包”、“静态生成”、“动态生成”三种方式来自动生成方法、注入并执行。 如下代码所示:
代码清单一: AutoGeneratingMethods.groovy
class AutoGeneratingMethods {
def can(skill) {
return { ->
println "i can $skill"
}
}
def canAdvanced(skill) {
AutoGeneratingMethods.metaClass."$skill" = { ->
println "i can $skill advanced."
}
}
static void main(args) {
def agm = new AutoGeneratingMethods()
def skills = ['swim', 'piano', 'writing']
skills.each {
agm.can(it)()
}
println("using closure: class methods: " + AutoGeneratingMethods.metaClass.methods.collect { it.name })
println("using closure: object methods: " + agm.metaClass.methods.collect { it.name })
def agm2 = new AutoGeneratingMethods()
def newNewSkills = ['rocking', 'travel', 'climbing']
newNewSkills.each {
def thisSkill = it
agm2.metaClass."$it" = { ->
println "i can $thisSkill dynamically"
}
agm2."$it"()
}
println("use object injecting: class methods: " + AutoGeneratingMethods.metaClass.methods.collect { it.name })
println("use object injecting: object methods: " + agm2.metaClass.methods.collect { it.name })
def agm3 = new AutoGeneratingMethods()
def newSkills = ['dance', 'drawing', 'thinking']
newSkills.each {
agm3.canAdvanced(it)()
}
println("using class method injecting: class methods: " + AutoGeneratingMethods.metaClass.methods.collect { it.name })
println("using class method injecting: object methods: " + agm3.metaClass.methods.collect { it.name })
}
}
第一种方法中,会将 skill 绑定到闭包内,实际上会有副作用; 第二种方法,是直接在对象上定义新的方法并调用; 第三种方法,是在类 AutoGeneratingMethods 的元类上定义方法并注入,然后运行。 这就是自动生成方法的基本示例了。
关键点:使用 ."$methodName" = { 闭包 } 来动态注入方法。
首先来看之前的测试代码怎么写的:
public class OldTestCase {
@TestMethod
String testSearchOrderType() {
//conditions: orderTypeDesc = 'someType' eg. NORMAL
//return validations: order_type = 'value for someType' eg. 0 for each order
def orderTypeMap = ["NORMAL" :0,
"GROUP" :10]
getFinalResult orderTypeMap.collect {
orderTypeDesc, returnValue ->
GeneralOrderSearchParam orderSearchParam = ParamUtil.
buildGeneralOrderSearchParam(kdtId)
orderSearchParam.getOrderSearchParam().setOrderTypeDesc([orderTypeDesc])
PlainResult<SearchResultModel> searchResult = generalOrderSearchService.
search(orderSearchParam)
assertSearchResult(searchResult, 'order_type', returnValue, orderSearchParam)
}
}
@TestMethod
String testSearchOrderState() {
//conditions: stateDesc = 'someState' eg. TOPAY
//return validations: state = 'value for someState' eg. 1 for each order
def orderStateMap = ["TOPAY" :1,
"SUCCESS":100]
getFinalResult orderStateMap.collect {
orderState, returnValue ->
GeneralOrderSearchParam orderSearchParam = ParamUtil.
buildGeneralOrderSearchParam(kdtId)
orderSearchParam.getOrderSearchParam().setStateDesc([orderState])
PlainResult<SearchResultModel> searchResult = generalOrderSearchService.
search(orderSearchParam)
assertSearchResult(searchResult, 'state', returnValue, orderSearchParam)
}
}
@TestMethod
String testCombinedFieldsSearch() {
//conditions: recName = qin && orderTypeDesc = NORMAL
//return validations: rec_name = 'qin' , order_type = 0 for each order
def compositeSearch = [new SingleSearchTestCase('recName', 'rec_name', 'qin',
'qin'), new SingleSearchTestCase(
'orderTypeDesc', 'order_type',
'NORMAL', 0)]
commonGeneralOrderSearchTest(new CompositeSearchTestCase(compositeSearch))
return GlobalConstants.SUCCESS
}
}
可见, 原来的写法,1. 没有将测试数据(枚举)和测试代码分离; 2. 要定义一些固定对象,不够灵活;3. 不同的测试要写不同的代码,不够通用。
怎么写法能够“一统天下”呢 ?
仔细来分析下测试用例, 它包含如下两个要素:
只要将这两部分配置化即可。 于是,可以定义测试用例元数据如下:
define test data metap structure:
{
params: {
'searchCondField1': searchValue1,
'searchCondField2': searchValue2
},
validations: {
'validationField1': value1,
'validationField2': value2
}
}
解析这个元数据,获得入参对和校验对,然后根据两者来编写测试代码:
代码清单二: AutoGeneratingTestsPlain.groovy
import groovy.util.logging.Log
@Log
class AutoGeneratingTestsPlain {
def static generateTest(testData) {
def orderSearchParam = new OrderSearchParam()
testData.params.each { pprop, pvalue ->
orderSearchParam."$pprop" = pvalue
}
log.info(JSON.toJSONString(orderSearchParam))
def result = mockSearch(orderSearchParam)
assert result.code == 200
assert result.msg == 'success'
result.orders.each { order ->
testData.validations.each { vdField, vdValue ->
assert order."$vdField" == vdValue
}
}
log.info("test passed.")
}
static void main(args) {
AutoGeneratingTestsPlain.generateTest(
[
params: [
'orderTypeDesc': ['NORMAL'],
'recName': 'qin'
],
validations: [
'order_type': 0,
'rec_name': 'qin'
]
]
)
}
def static mockSearch(orderSearchParam) {
def results = new Expando(msg: 'success' , code: 200)
results.orders = (1..20).collect {
new Expando(order_type:0 , rec_name: 'qin')
}
results
}
}
AutoGeneratingTestsPlain.generateTest 展示了新的写法。 这个测试代码流程可以说非常清晰了。设置入参,调用接口,校验返回,一气呵成。
不过,这个方法是写死的,如果我要定义新的测试用例,就不得不编写新的测试方法。 可以将这里面的测试方法体,抽离出来,变成一个动态方法注入。
如下代码所示。 将原来的测试方法体抽离出来,变成 AutoGeneratingTestsUsingMetap 的元类的动态方法注入,然后调用运行。这样,就可以根据不同的测试用例数据,生成对应的测试方法,然后注入和运行。 是不是更加灵活了?
注意到,与上面不一样的是,这里每一个测试用例都会生成一个单独的测试方法,有一个独有的测试方法名称。而上面的例子,只有一个 generateTest 用来执行测试用例逻辑。
代码清单三: AutoGeneratingTestsUsingMetap.groovy
@Log
class AutoGeneratingTestsUsingMetap {
def static generateTest(testData) {
def testMethodName = "test${testData.params.collect { "$it.key = $it.value" }.join('_')}"
AutoGeneratingTestsUsingMetap.metaClass."$testMethodName" = { tdata ->
def orderSearchParam = new OrderSearchParam()
tdata.params.each { pprop, pvalue ->
orderSearchParam."$pprop" = pvalue
}
log.info(JSON.toJSONString(orderSearchParam))
def result = mockSearch(orderSearchParam)
assert result.code == 200
assert result.msg == 'success'
result.orders.each { order ->
tdata.validations.each { vdField, vdValue ->
assert order."$vdField" == vdValue
}
}
log.info("test passed.")
}(testData)
println(AutoGeneratingTestsUsingMetap.metaClass.methods.collect{ it.name })
}
static void main(args) {
AutoGeneratingTestsUsingMetap.generateTest(
[
params: [
'orderTypeDesc': ['NORMAL'],
'recName': 'qin'
],
validations: [
'order_type': 0,
'rec_name': 'qin'
]
]
)
}
def static mockSearch(orderSearchParam) {
def results = new Expando(msg: 'success' , code: 200)
results.orders = (1..20).collect {
new Expando(order_type:0 , rec_name: 'qin')
}
results
}
}
测试方法已经可以动态生成,接下来,就是动态生成测试用例数据了。 这个可以根据具体的测试值,来自动生成。比如说,有一个 orderType 的枚举配置, ["NORMAL" :0, "GROUP" :10], 完整的应该是:[[‘orderTypeDesc‘:[‘NORMAL‘], ‘order_type‘: 0], [‘orderTypeDesc‘:[‘GROUP‘], ‘order_type‘: 10]] 可以写个方法来生成指定的测试用例数据,做个结构转换即可。
本文通过元编程的方法,重新思考和自动构造了订单搜索接口的测试用例集合,并使之更加清晰、灵活可配置。要应用元编程,定义清晰的元数据结构,是非常必要的基础工作。元编程实质上就是基于元数据做一些自动的类、方法、变量注入。
此外,从不同的思维视角来看待同一件事物是有益的。
原文:https://www.cnblogs.com/lovesqcc/p/10847298.html