在 「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 結尾的測試數據靜態變量, 就能夠自動生成測試用例集合,並自動執行自動生成的測試方法啦。
遺留的問題:複合搜索條件的測試用例怎麼自動生成呢 ? 這個就留給讀者去思考啦!
本文經過元編程的方法,從新思考和自動構造了訂單搜索接口的測試用例集合,並使之更加清晰、靈活可配置。要應用元編程,定義清晰的元數據結構,是很是必要的基礎工做。元編程實質上就是基於元數據作一些自動的類、方法、變量注入。
當須要編寫一些類似的重複代碼時,不妨先定義一些元數據結構和應用模板,並基於此來自動生成相關的代碼。此外,從不一樣的思惟視角來看待同一件事物是有益的。