• Groovy元编程应用之自动生成订单搜索接口测试用例集


    背景###

    “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. 不同的测试入参要写相似的模板代码,不够通用。

    定义测试用例元数据####

    怎么写法能够“一统天下”呢 ?

    仔细来分析下测试用例, 它包含如下两个要素:

    • 入参: 参数名称 和 参数值; 入参可以包含多个参数对。
    • 校验: 返回字段 和 校验值; 校验也可以包含多个基本字段校验。

    只要将这两部分配置化即可。 于是,可以定义测试用例元数据结构如下:

    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 结尾的测试数据静态变量, 就可以自动生成测试用例集合,并自动执行自动生成的测试方法啦。

    遗留的问题:复合搜索条件的测试用例怎么自动生成呢 ? 这个就留给读者去思考啦!


    小结###

    本文通过元编程的方法,重新思考和自动构造了订单搜索接口的测试用例集合,并使之更加清晰、灵活可配置。要应用元编程,定义清晰的元数据结构,是非常必要的基础工作。元编程实质上就是基于元数据做一些自动的类、方法、变量注入。

    当需要编写一些相似的重复代码时,不妨先定义一些元数据结构和应用模板,并基于此来自动生成相关的代码。此外,从不同的思维视角来看待同一件事物是有益的。

  • 相关阅读:
    ASP.NET实现写入和读取图片(C#+SQL Server)
    nginx for windows: 让nginx以服务的方式运行(亲侧修正)
    开源射击FPS游戏Blood Frontier Beta 2发布
    批处理更改IP
    Javascript 函数: CTRL+回车 提交表单
    汇总Javascript各种判断脚本
    Linux在好莱坞战胜了微软?
    SATA硬盘和IDE硬盘共存问题
    总结性知识:107个常用Javascript语句
    ASP.NET调用javascript脚本的方法总结
  • 原文地址:https://www.cnblogs.com/lovesqcc/p/10847298.html
Copyright © 2020-2023  润新知