在 “Groovy元编程简明教程” 一文中,简明地介绍了 Groovy 元编程的特性。 那么,元编程能够应用哪些场合呢?元编程一般能够用来自动生成一些类似的模板代码。html
在 “使用Groovy+Spock构建可配置的订单搜索接口测试用例集” 一文中,谈到了如何将搜索接口的测试用例配置化。 不过,那还只是初级配置化, 含有浓浓的 Java 对象味了, 测试代码与测试用例集合的配置实际上并无分离,整个测试方法看起来不够清晰。 那么,用元编程的方法,会是怎样呢 ?
编程
首先,来看一个简单的例子。 这个例子使用了“闭包”、“静态生成”、“动态生成”三种方式来自动生成方法、注入并执行。 以下代码所示:json
代码清单一: 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" = { 闭包 } 来动态注入方法。测试
首先来看以前的测试代码怎么写的:ui
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. 不一样的测试入参要写类似的模板代码,不够通用。
this
怎么写法可以“一统天下”呢 ?code
仔细来分析下测试用例, 它包含以下两个要素:htm
只要将这两部分配置化便可。 因而,能够定义测试用例元数据结构以下:
define meta structure of test case : { params: { 'searchCondField1': searchValue1, 'searchCondField2': searchValue2, 'searchCondFieldN': searchValueN, }, validations: { 'validationField1': value1, 'validationField2': value2, 'validationFieldN': valueN, } }
解析这个元数据,得到入参对和校验对,而后根据二者来编写测试代码:
代码清单二: AutoGeneratingTestsPlain.groovy
import com.alibaba.fastjson.JSON import groovy.util.logging.Log @Log class AutoGeneratingTestsPlain { def static generateTest(testCase) { def orderSearchParam = new OrderSearchParam() testCase.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 -> testCase.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
import com.alibaba.fastjson.JSON import groovy.util.logging.Log @Log class AutoGeneratingTestsUsingMetap { def static generateTests(testCases) { testCases.each { generateTest(it) } } def static generateTest(testCase) { def testMethodName = "test${testCase.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.") }(testCase) 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], 完整的能够定义为:['condField':'orderTypeDesc', 'validationField': 'order_type', 'valuePair': ["NORMAL" :0,"GROUP" :10]] 能够写个方法来生成指定的测试用例数据,作个结构转换便可。
代码清单四:AutoGeneratingTestData.groovy
class AutoGeneratingTestData { def static orderTypeTestData = ['condField':'orderTypeDesc', 'validationField': 'order_type', 'valuePair': [["NORMAL"] :0, ["GROUP"] :10]] def static stateTestData = ['condField':'stateDesc', 'validationField': 'state', 'valuePair': ["TOPAY" :1, "SUCCESS":100]] def static generateAllTestCases(testDatas) { testDatas.collect { generateTestCases(it) }.flatten() } def static generateTestCases(testData) { testData.valuePair.collect { key, value -> def searchCondField = testData['condField'] def validationField = testData['validationField'] return [ params: [ "$searchCondField": key ], validations: [ "$validationField": value ] ] } } static void main(args) { println AutoGeneratingTestData.generateTestCases(orderTypeTestData) println AutoGeneratingTestData.generateTestCases(stateTestData) } }
接下来,能够把全部这些衔接起来:
代码清单五:PutingAllTogether.groovy
class PutingAllTogether { static void main(args) { def testDatas = AutoGeneratingTestData.declaredFields.grep { it.name.endsWith('TestData') }.collect { it.get(AutoGeneratingTestData.class) } def testCases = AutoGeneratingTestData.generateAllTestCases(testDatas) AutoGeneratingTestsUsingMetap.generateTests(testCases) } }
如今,只要在 AutoGeneratingTestData 添加以 TestData 结尾的测试数据静态变量, 就能够自动生成测试用例集合,并自动执行自动生成的测试方法啦。
遗留的问题:复合搜索条件的测试用例怎么自动生成呢 ? 这个就留给读者去思考啦!
本文经过元编程的方法,从新思考和自动构造了订单搜索接口的测试用例集合,并使之更加清晰、灵活可配置。要应用元编程,定义清晰的元数据结构,是很是必要的基础工做。元编程实质上就是基于元数据作一些自动的类、方法、变量注入。
当须要编写一些类似的重复代码时,不妨先定义一些元数据结构和应用模板,并基于此来自动生成相关的代码。此外,从不一样的思惟视角来看待同一件事物是有益的。