点击文末小卡片免费获取软件测试全套资料资料在手涨薪更快接口测试三要素参数构造发起请求获取响应校验结果一、原始状态当我们的用例没有进行分层设计的时候只能算是一个“苗条式”的脚本。以一个后台创建商品活动的场景为例大概流程是这样的(默认已经是登录状态下)创建商品-创建分类-创建优惠券-创建活动要进行接口测试的话按照接口测试的三要素来进行具体的效果如下# 1、参数构造 createCommodityParams { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } } createCategoryParams {......} createCouponParams {......} createPublicityParams {......} publishCommodityParams {......} publishPublicityParams {......} createCommodityParams[input][title] autoTest str(time.time()) createCommodityParams[input][mallCode] self.mallCode createCommodityParams[input][skuList][0][price] random.randint(1,10) createCategoryParams[input][categoryName] autoTestCategory str(time.time()) createCouponParams。。。 createPublicityParams。。。 publishCommodityParams。。。 publishPublicityParams。。。 # 2、发起请求获取响应 # 创建商品并获取商品code createCommodityRes api.getUrl(testApi.create.commodity).post.params(createCommodityParams) commodityCode createCommodityRes[commodityCode] # 创建分类并获取分类code createCategoryRes api.getUrl(testApi.create.category).post.params(createCategoryParams) categoryCode createCategoryRes[categoryCode] # 创建优惠券并获取优惠券code createCouponRes api.getUrl(testApi.create.coupon).post.params(createCouponParams) couponCode createCouponRes[couponCode] # 创建活动并关联商品,绑定优惠券,设置分类 createPublicityParams[input][commodityCode] commodityCode createPublicityParams[input][categoryCode] categoryCode createPublicityParams[input][couponCode] couponCode createPublicityRes api.getUrl(testApi.create.publicity).post.params(createPublicityParams) # 结果校验(断言) assert.equal(createPublicityRes[code], 0) assert.equal(createPublicityRes[publicityName], createPublicityParams[publicityName]) 。。。按照上面的写法对于单个脚本的调式来说或许可以但是一旦用例的数量和复杂程度积累起来后其维护成本将是巨大的或者可以说不具备可维护性。弊端说明可读性差所有的处理都放在一起代码量大不简洁直观灵活性差参数写死在脚本适用用例范围小复用性差如果其他用例需要同样或类似的步骤需要重新写一份维护性差如果接口有任何改动那么所有涉及到此接口的脚本都需要一一修改例如随着用例场景的增加就可能会出现下面这种情况按照原始的模式我们就需要些3个脚本文件分别来描述着3个场景并且创建商品_API、创建分类_API、创建优惠券_API在场景123中均出现了上架商品_API在场景23中均出现。由此我们完全可以预见到当几百上千的用例场景出现后这种形式是没有维护性可言的。二、进化历程因此我们依照着痛点以最开始的原始状态为例对用例进行分层改造来看看进化后的状态。1、API 定义层我们编程的时候会将一些重复的代码进行封装使用那么这里依然可以借用这种思想我们将 API 的定义单独抽离单独定义。我们期望的效果是这样的提前将API的定义放在一层供用例场景引用这样当接口有任何修改时我们只需要修改API definition层即可。实例演示对应着上面的demo我们就是需要做如下抽离class APIDefinition:‘’’创建商品API定义createCommodityParams 创建商品接口入参return创建商品接口响应结果‘’’def createCommodityRequest(createCommodityParams):return api.getUrl(“testApi.create.commodity”).post.params(createCommodityParams) 创建分类API定义 createCategoryParams 创建分类接口入参 return创建分类接口响应结果 def createCategoryRequest(createCategoryParams) return api.getUrl(testApi.create.category).post.params(createCategoryParams) # 创建优惠券接口定义 def createCouponRequest(createCouponParams) return api.getUrl(testApi.create.coupon).post.params(createCouponParams) # 创建活动接口定义 def createPublicityRequest(createPublicityParams) return api.getUrl(testApi.create.publicity).post.params(createPublicityParams) # ...其余省略2、Service 层上面我们已经将接口的定义抽离出来解决了 API 重复定义的问题但是再继续分析会发现有一个问题依然没有解决就是场景的复用性.再看刚才的图3个场景中都有重复的步骤类似创建商品、创建分类、创建优惠券这些,并且这些步骤都是一个个API的组合一个步骤对应一个API在各个步骤之间还会有数据的处理与传递为了解决这些问题将对场景再次做抽离这里我称之为service层。这一层之所以叫做service(服务)层是因为它的作用是用来提供测试用例所需要的各种“服务”好比参数构建、接口请求、数据处理、测试步骤。用下图先来看分层的目标我们希望将常用的测试场景步骤封装至service层中供用例场景调用增加复用性也可以理解为测试用例的前置处理但是这里还是有一点小问题就是service层的东西太多太杂有些场景步骤可能只适用于我当前的项目用例在实际的工作中各个系统间是相互依赖的前台APP的测试很大可能就依赖后台创建作为前置条件好比我在APP端只要商品和分类可能只想创建商品和分类并不想创建优惠券这个时候service层就没有适用的场景步骤供调用那么我就需要根据自己的需要重新封装可是对于很多单接口的前置数据处理又是一致的比如createCommodityParams[input][title] autoTest str(time.time()) createCommodityParams[input][mallCode] self.mallCode createCommodityParams[input][skuList][0][price] random.randint(1,10) createCategoryParams[input][categoryName] autoTestCategory str(time.time()) createCouponParams。。。 createPublicityParams。。。 publishCommodityParams。。。 publishPublicityParams。。。重新封装的话还要再处理这一步就有点麻烦且不符合我们的复用性设计了因此我们对service层再细化为3层分别为apiObject单接口的预处理层这一层主要作用是单接口入参的构造接口的请求与响应值返回每个接口请求不依赖与业务步骤都是单接口的请求此外一些简单固定的入参构建也直接放在这里处理比如随机的商品名title等和具体业务流程无关针对所有调用此接口的场景均适用caseService多接口的预处理层这一层主要是测试步骤teststep或场景的有序集合。用例所需要的步骤通过每一个请求进行组合每一个步骤都对应着一个API请求这些步骤会组成一个个场景各个场景之间可以互相调用组成新的场景以适应不同的测试用例需求。场景封装好以后可以供不同的测试用例调用除了当前项目的用例其他业务线需要的话也可从此caseService中选择调用提高复用性的同时也避免了用例相互依赖的问题。util这一层主要放置针对当前业务的接口需要处理的数据在实际编写测试步骤时可能部分接口的参数是通过其他接口获取后经过处理才可以使用或是修改数据格式或是修改字段名称亦或是某些 value 的加解密处理等。细化分层后各层的职责便更加清晰明确具体如下图实例演示apiObject:class ApiObject: def createCommodity(createCommodityParams): inputParams ApiParamsBuild().createCommodityParamsBuild(createCommodityParams) response APIDefinition().createCommodityRequest(inputParams) return response def createCategory(createCategoryParams): ... def createCoupon(createCouponParams): ... ...... class ApiParamsBuild: def createCommodityParamsBuild(createCommodityParams): createCommodityParams[input][title] autoTest str(time.time()) createCommodityParams[input][mallCode] self.mallCode createCommodityParams[input][skuList][0][price] random.randint(1,10) return createCommodityParams def createCategoryParamsBuild(createCategoryParams): ... def createCouponParamsBuild(createCouponParams): ... ......到此我们来看看原始的用例经过目前封装后的模样1、参数构造createCommodityParams { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } } createCategoryParams {......} createCouponParams {......} createPublicityParams {......} publishCommodityParams {......} publishPublicityParams {......} # 2、发起请求获取响应 # 创建商品并获取商品code createCommodityRes ApiObject().createCommodity(createCommodityParams) commodityCode createCommodityRes[commodityCode] # 创建分类并获取分类code createCategoryRes ApiObject().createCategory(createCategoryParams) categoryCode createCategoryRes[categoryCode] # 创建优惠券并获取优惠券code createCouponRes ApiObject().createCoupon(createCouponParams) couponCode createCouponRes[couponCode] # 创建活动并关联商品,绑定优惠券,设置分类 createPublicityParams[input][commodityCode] commodityCode createPublicityParams[input][categoryCode] categoryCode createPublicityParams[input][couponCode] couponCode createPublicityRes ApiObject().createPublicity(createPublicityParams) # 结果校验(断言) assert.equal(createPublicityRes[code], 0) assert.equal(createPublicityRes[publicityName], createPublicityParams[publicityName]) 。。。可以看到现在接口请求的url、method、通用入参处理等已经不会在用例中体现了接下来继续封装caseService层。 caseService: 我们将多接口的场景步骤进行封装 class CaseService: def createPublicityByCategory(params): # 创建商品并获取商品code createCommodityRes ApiObject().createCommodity(createCommodityParams) commodityCode createCommodityRes[commodityCode] # 创建分类并获取分类code createCategoryRes ApiObject().createCategory(createCategoryParams) categoryCode createCategoryRes[categoryCode] # 创建优惠券并获取优惠券code createCouponRes ApiObject().createCoupon(createCouponParams) couponCode createCouponRes[couponCode] # 创建活动并关联商品,绑定优惠券,设置分类 createPublicityParams[input][commodityCode] commodityCode createPublicityParams[input][categoryCode] categoryCode createPublicityParams[input][couponCode] couponCode createPublicityRes ApiObject().createPublicity(createPublicityParams) return createPublicityRes ......这时体现在用例中的表现就如下层testcase层所示.3、testcase 层我们想要的是一个清晰明了“一劳永逸”的自动化测试用例就像我们的手工测试用例一样我们的前置条件可以复用我们入参可以任意修改但测试步骤都是固定不变的(前提可能是产品没有偷偷改需求~)。这一层其实是对应的testsuite(测试用例集)是测试用例的无序集合。其中各个用例之间应该是相互独立互不干扰不存在依赖关系每个用例都可以单独运行。最终我们期望自动化用例的维护过程中达到的效果如下testcase 层# 1、参数构造 createCommodityParams { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } } createCategoryParams {......} createCouponParams {......} createPublicityParams {......} publishCommodityParams {......} publishPublicityParams {......} # 2、发起请求获取响应 createPublicityRes CaseService().createPublicityByCategory(createCommodityParams,createCategoryParams,createCouponParams...) # 结果校验(断言) assert.equal(createPublicityRes[code], 0) assert.equal(createPublicityRes[publicityName], createPublicityParams[publicityName]) 。。。可以看到这时涉及到用例场景步骤的代码已经非常少了并且完全独立与框架、其他用例等均无耦合。到这里我们再看用例会发现一点测试数据依然冗长那么下面就开始对测试数据进行参数化和数据驱动的处理。4、testdata此层用来管理测试数据作为参数化场景的数据驱动。参数化 所谓参数化简单来说就是将入参利用变量的形式传入不要将参数写死增加灵活性好比搜索商品的接口不同的关键字和搜索范围作为入参就会得到不同的搜索结果。上面的例子中其实已经是参数化了。数据驱动对于参数我们可以将其放入一个文件中可以存放多个入参形成一个参数列表的形式然后从中读取参数传入接口即可。常见做数据驱动的有 JSON、CSV、YAML 等。实例演示我们以CSV为例不特别依照某个框架通常测试框架都具备参数化的功能。将所需要的入参放入test.csv文件中createCommodityParams,createCategoryParams,... { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } }, ...然后再回到用例层利用框架参数化的功能对数据进行读取# 1、参数构造 parametrize(params readCsv(test.csv)) # 2、发起请求获取响应 createPublicityRes CaseService().createPublicityByCategory(params) # 结果校验(断言) assert.equal(createPublicityRes[code], 0) assert.equal(createPublicityRes[publicityName], createPublicityParams[publicityName]) 。。。注这里的测试数据不仅仅局限于接口的请求参数既然做数据驱动那么断言也可以维护在此以减少用例层的代码冗余。5、rawData这一层是存放接口原始入参的地方。某些接口的入参可能很多其中很多参数值又可能是固定不变的构建入参的时候我们只想对变的值进行动态的维护而不维护的值就使用原始参数中的默认值以此减少工作量(emmm…可能也就是CV大法的量吧~)再者就是数据驱动的数据文件中只维护需要修改的参数使数据文件更简洁可阅读性更强。实例演示这种利用原始参数(rawData)的方法我们称之为模板化实际工作中有多种方式可实现例如jsonpath、Mustache或者自己根据需求实现方法,本文重点在介绍分层设计所以就不具体演示模板化技术的细节了仅说明设计此层的作用。以实例中的入参createCommodityParams为例未用模板化技术前我们要在CSV里面维护完整的入参createCommodityParams,createCategoryParams,... { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } }, ...但是实际上我们可能仅仅需要修改维护其中某个或某几个字段例如只想维护商品价格其余的使用默认值即可使用模板化技术后可能在CSV中就是这样的表现createCommodityParams,createCategoryParams,... { input: { skuList: [ { price: 1, retailPrice: 6 } }, ...或者这样- keyPath: $.input.skuList[0].price value: 1 - keyPath: $.input.skuList[0].retailPrice value: 6亦或使用Mustache将需要修改的value进行参数化{{value}}。我们可以看到这样处理后的数据驱动的文件就变得简洁清晰的许多当一个文件中维护了多个用例且入参字段很多时这样维护起来就可以清晰的看出每个数据对应的用例的作用了price就是为了测试价格的stock就是为了测试库存的publish就是为了测试上下架的等等。注 当然此层的使用视实际情况而定有可能这个接口的参数本身就没多少那么直接全量使用就行或者你就是觉得数据量哪怕再大我都能分得清楚看的明白不用也rawData是可以的~6、Base此层主要放置我们需要处理的公共前置条件和一些自动化公共方法也可以理解为公共的config和util。在我们实际的自动化开发过程中有很多前置条件或公共方法比如登录处理log 处理断言方法或一些数据处理使用过程中所有的service和testcase层都会继承此类这样这些公共方法和前置条件便可直接通用在各个业务线之间也可保持一致性。三、完结最后我们来看下整体分层后的目录结构总览└─apiautotest └─project └─rawData原始参数 ├─testRawData.json └─service用例服务 └─apiObject单接口预处理单接口入参的构造接口的请求与响应值返回 ├─testApiObject.py └─caseService多接口预处理测试步骤teststep或场景的有序集合 ├─testCaseService.py └─util工具类 ├─util.py └─testcase测试用例 └─testDataDriven测试数据驱动 ├─testData.csv ├─testcase.py测试用例集 └─testBase.py测试基类初始化和公共方法 └─platformapiApi定义 ├─testApiDefinition.py最后感谢每一个认真阅读我文章的人礼尚往来总是要有的虽然不是什么很值钱的东西如果你用得到的话可以直接拿走这些资料对于做【软件测试】的朋友来说应该是最全面最完整的备战仓库这个仓库也陪伴我走过了最艰难的路程希望也能帮助到你凡事要趁早特别是技术行业一定要提升技术功底。
接口自动化测试分层设计与实践总结
点击文末小卡片免费获取软件测试全套资料资料在手涨薪更快接口测试三要素参数构造发起请求获取响应校验结果一、原始状态当我们的用例没有进行分层设计的时候只能算是一个“苗条式”的脚本。以一个后台创建商品活动的场景为例大概流程是这样的(默认已经是登录状态下)创建商品-创建分类-创建优惠券-创建活动要进行接口测试的话按照接口测试的三要素来进行具体的效果如下# 1、参数构造 createCommodityParams { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } } createCategoryParams {......} createCouponParams {......} createPublicityParams {......} publishCommodityParams {......} publishPublicityParams {......} createCommodityParams[input][title] autoTest str(time.time()) createCommodityParams[input][mallCode] self.mallCode createCommodityParams[input][skuList][0][price] random.randint(1,10) createCategoryParams[input][categoryName] autoTestCategory str(time.time()) createCouponParams。。。 createPublicityParams。。。 publishCommodityParams。。。 publishPublicityParams。。。 # 2、发起请求获取响应 # 创建商品并获取商品code createCommodityRes api.getUrl(testApi.create.commodity).post.params(createCommodityParams) commodityCode createCommodityRes[commodityCode] # 创建分类并获取分类code createCategoryRes api.getUrl(testApi.create.category).post.params(createCategoryParams) categoryCode createCategoryRes[categoryCode] # 创建优惠券并获取优惠券code createCouponRes api.getUrl(testApi.create.coupon).post.params(createCouponParams) couponCode createCouponRes[couponCode] # 创建活动并关联商品,绑定优惠券,设置分类 createPublicityParams[input][commodityCode] commodityCode createPublicityParams[input][categoryCode] categoryCode createPublicityParams[input][couponCode] couponCode createPublicityRes api.getUrl(testApi.create.publicity).post.params(createPublicityParams) # 结果校验(断言) assert.equal(createPublicityRes[code], 0) assert.equal(createPublicityRes[publicityName], createPublicityParams[publicityName]) 。。。按照上面的写法对于单个脚本的调式来说或许可以但是一旦用例的数量和复杂程度积累起来后其维护成本将是巨大的或者可以说不具备可维护性。弊端说明可读性差所有的处理都放在一起代码量大不简洁直观灵活性差参数写死在脚本适用用例范围小复用性差如果其他用例需要同样或类似的步骤需要重新写一份维护性差如果接口有任何改动那么所有涉及到此接口的脚本都需要一一修改例如随着用例场景的增加就可能会出现下面这种情况按照原始的模式我们就需要些3个脚本文件分别来描述着3个场景并且创建商品_API、创建分类_API、创建优惠券_API在场景123中均出现了上架商品_API在场景23中均出现。由此我们完全可以预见到当几百上千的用例场景出现后这种形式是没有维护性可言的。二、进化历程因此我们依照着痛点以最开始的原始状态为例对用例进行分层改造来看看进化后的状态。1、API 定义层我们编程的时候会将一些重复的代码进行封装使用那么这里依然可以借用这种思想我们将 API 的定义单独抽离单独定义。我们期望的效果是这样的提前将API的定义放在一层供用例场景引用这样当接口有任何修改时我们只需要修改API definition层即可。实例演示对应着上面的demo我们就是需要做如下抽离class APIDefinition:‘’’创建商品API定义createCommodityParams 创建商品接口入参return创建商品接口响应结果‘’’def createCommodityRequest(createCommodityParams):return api.getUrl(“testApi.create.commodity”).post.params(createCommodityParams) 创建分类API定义 createCategoryParams 创建分类接口入参 return创建分类接口响应结果 def createCategoryRequest(createCategoryParams) return api.getUrl(testApi.create.category).post.params(createCategoryParams) # 创建优惠券接口定义 def createCouponRequest(createCouponParams) return api.getUrl(testApi.create.coupon).post.params(createCouponParams) # 创建活动接口定义 def createPublicityRequest(createPublicityParams) return api.getUrl(testApi.create.publicity).post.params(createPublicityParams) # ...其余省略2、Service 层上面我们已经将接口的定义抽离出来解决了 API 重复定义的问题但是再继续分析会发现有一个问题依然没有解决就是场景的复用性.再看刚才的图3个场景中都有重复的步骤类似创建商品、创建分类、创建优惠券这些,并且这些步骤都是一个个API的组合一个步骤对应一个API在各个步骤之间还会有数据的处理与传递为了解决这些问题将对场景再次做抽离这里我称之为service层。这一层之所以叫做service(服务)层是因为它的作用是用来提供测试用例所需要的各种“服务”好比参数构建、接口请求、数据处理、测试步骤。用下图先来看分层的目标我们希望将常用的测试场景步骤封装至service层中供用例场景调用增加复用性也可以理解为测试用例的前置处理但是这里还是有一点小问题就是service层的东西太多太杂有些场景步骤可能只适用于我当前的项目用例在实际的工作中各个系统间是相互依赖的前台APP的测试很大可能就依赖后台创建作为前置条件好比我在APP端只要商品和分类可能只想创建商品和分类并不想创建优惠券这个时候service层就没有适用的场景步骤供调用那么我就需要根据自己的需要重新封装可是对于很多单接口的前置数据处理又是一致的比如createCommodityParams[input][title] autoTest str(time.time()) createCommodityParams[input][mallCode] self.mallCode createCommodityParams[input][skuList][0][price] random.randint(1,10) createCategoryParams[input][categoryName] autoTestCategory str(time.time()) createCouponParams。。。 createPublicityParams。。。 publishCommodityParams。。。 publishPublicityParams。。。重新封装的话还要再处理这一步就有点麻烦且不符合我们的复用性设计了因此我们对service层再细化为3层分别为apiObject单接口的预处理层这一层主要作用是单接口入参的构造接口的请求与响应值返回每个接口请求不依赖与业务步骤都是单接口的请求此外一些简单固定的入参构建也直接放在这里处理比如随机的商品名title等和具体业务流程无关针对所有调用此接口的场景均适用caseService多接口的预处理层这一层主要是测试步骤teststep或场景的有序集合。用例所需要的步骤通过每一个请求进行组合每一个步骤都对应着一个API请求这些步骤会组成一个个场景各个场景之间可以互相调用组成新的场景以适应不同的测试用例需求。场景封装好以后可以供不同的测试用例调用除了当前项目的用例其他业务线需要的话也可从此caseService中选择调用提高复用性的同时也避免了用例相互依赖的问题。util这一层主要放置针对当前业务的接口需要处理的数据在实际编写测试步骤时可能部分接口的参数是通过其他接口获取后经过处理才可以使用或是修改数据格式或是修改字段名称亦或是某些 value 的加解密处理等。细化分层后各层的职责便更加清晰明确具体如下图实例演示apiObject:class ApiObject: def createCommodity(createCommodityParams): inputParams ApiParamsBuild().createCommodityParamsBuild(createCommodityParams) response APIDefinition().createCommodityRequest(inputParams) return response def createCategory(createCategoryParams): ... def createCoupon(createCouponParams): ... ...... class ApiParamsBuild: def createCommodityParamsBuild(createCommodityParams): createCommodityParams[input][title] autoTest str(time.time()) createCommodityParams[input][mallCode] self.mallCode createCommodityParams[input][skuList][0][price] random.randint(1,10) return createCommodityParams def createCategoryParamsBuild(createCategoryParams): ... def createCouponParamsBuild(createCouponParams): ... ......到此我们来看看原始的用例经过目前封装后的模样1、参数构造createCommodityParams { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } } createCategoryParams {......} createCouponParams {......} createPublicityParams {......} publishCommodityParams {......} publishPublicityParams {......} # 2、发起请求获取响应 # 创建商品并获取商品code createCommodityRes ApiObject().createCommodity(createCommodityParams) commodityCode createCommodityRes[commodityCode] # 创建分类并获取分类code createCategoryRes ApiObject().createCategory(createCategoryParams) categoryCode createCategoryRes[categoryCode] # 创建优惠券并获取优惠券code createCouponRes ApiObject().createCoupon(createCouponParams) couponCode createCouponRes[couponCode] # 创建活动并关联商品,绑定优惠券,设置分类 createPublicityParams[input][commodityCode] commodityCode createPublicityParams[input][categoryCode] categoryCode createPublicityParams[input][couponCode] couponCode createPublicityRes ApiObject().createPublicity(createPublicityParams) # 结果校验(断言) assert.equal(createPublicityRes[code], 0) assert.equal(createPublicityRes[publicityName], createPublicityParams[publicityName]) 。。。可以看到现在接口请求的url、method、通用入参处理等已经不会在用例中体现了接下来继续封装caseService层。 caseService: 我们将多接口的场景步骤进行封装 class CaseService: def createPublicityByCategory(params): # 创建商品并获取商品code createCommodityRes ApiObject().createCommodity(createCommodityParams) commodityCode createCommodityRes[commodityCode] # 创建分类并获取分类code createCategoryRes ApiObject().createCategory(createCategoryParams) categoryCode createCategoryRes[categoryCode] # 创建优惠券并获取优惠券code createCouponRes ApiObject().createCoupon(createCouponParams) couponCode createCouponRes[couponCode] # 创建活动并关联商品,绑定优惠券,设置分类 createPublicityParams[input][commodityCode] commodityCode createPublicityParams[input][categoryCode] categoryCode createPublicityParams[input][couponCode] couponCode createPublicityRes ApiObject().createPublicity(createPublicityParams) return createPublicityRes ......这时体现在用例中的表现就如下层testcase层所示.3、testcase 层我们想要的是一个清晰明了“一劳永逸”的自动化测试用例就像我们的手工测试用例一样我们的前置条件可以复用我们入参可以任意修改但测试步骤都是固定不变的(前提可能是产品没有偷偷改需求~)。这一层其实是对应的testsuite(测试用例集)是测试用例的无序集合。其中各个用例之间应该是相互独立互不干扰不存在依赖关系每个用例都可以单独运行。最终我们期望自动化用例的维护过程中达到的效果如下testcase 层# 1、参数构造 createCommodityParams { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } } createCategoryParams {......} createCouponParams {......} createPublicityParams {......} publishCommodityParams {......} publishPublicityParams {......} # 2、发起请求获取响应 createPublicityRes CaseService().createPublicityByCategory(createCommodityParams,createCategoryParams,createCouponParams...) # 结果校验(断言) assert.equal(createPublicityRes[code], 0) assert.equal(createPublicityRes[publicityName], createPublicityParams[publicityName]) 。。。可以看到这时涉及到用例场景步骤的代码已经非常少了并且完全独立与框架、其他用例等均无耦合。到这里我们再看用例会发现一点测试数据依然冗长那么下面就开始对测试数据进行参数化和数据驱动的处理。4、testdata此层用来管理测试数据作为参数化场景的数据驱动。参数化 所谓参数化简单来说就是将入参利用变量的形式传入不要将参数写死增加灵活性好比搜索商品的接口不同的关键字和搜索范围作为入参就会得到不同的搜索结果。上面的例子中其实已经是参数化了。数据驱动对于参数我们可以将其放入一个文件中可以存放多个入参形成一个参数列表的形式然后从中读取参数传入接口即可。常见做数据驱动的有 JSON、CSV、YAML 等。实例演示我们以CSV为例不特别依照某个框架通常测试框架都具备参数化的功能。将所需要的入参放入test.csv文件中createCommodityParams,createCategoryParams,... { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } }, ...然后再回到用例层利用框架参数化的功能对数据进行读取# 1、参数构造 parametrize(params readCsv(test.csv)) # 2、发起请求获取响应 createPublicityRes CaseService().createPublicityByCategory(params) # 结果校验(断言) assert.equal(createPublicityRes[code], 0) assert.equal(createPublicityRes[publicityName], createPublicityParams[publicityName]) 。。。注这里的测试数据不仅仅局限于接口的请求参数既然做数据驱动那么断言也可以维护在此以减少用例层的代码冗余。5、rawData这一层是存放接口原始入参的地方。某些接口的入参可能很多其中很多参数值又可能是固定不变的构建入参的时候我们只想对变的值进行动态的维护而不维护的值就使用原始参数中的默认值以此减少工作量(emmm…可能也就是CV大法的量吧~)再者就是数据驱动的数据文件中只维护需要修改的参数使数据文件更简洁可阅读性更强。实例演示这种利用原始参数(rawData)的方法我们称之为模板化实际工作中有多种方式可实现例如jsonpath、Mustache或者自己根据需求实现方法,本文重点在介绍分层设计所以就不具体演示模板化技术的细节了仅说明设计此层的作用。以实例中的入参createCommodityParams为例未用模板化技术前我们要在CSV里面维护完整的入参createCommodityParams,createCategoryParams,... { input: { title: 活动商品, subtitle: , brand: , categoryLevel1Code: 12, categoryLevel2Code: 1312, categoryLevel3Code: 131211, detail: [ { uri: ecommerce/1118d9.jpg, type: 0 } ], installInfo: { installType: 1, installFee: null }, pictureList: [ { uri: ecommerce/222.jpg, main: true } ], postageInfo: { postageType: 2, postageFee: 1, postageId: null }, sellerDefinedCode: , publish: 1, skuList: [ { skuCode: , externalSkuCode: , price: 1, retailPrice: 6, stock: 100, weight: 0, suggestPrice: 0, skuAttrValueList: [ { attrCode: COLOR, attrName: 颜色, attrValue: 绿色, attrValueId: 1001 } ] } ], jumpSwitch:false, recommendCommodityCodeList: [], recommendFittingCodeList: [], mallCode: 8h4xxx } }, ...但是实际上我们可能仅仅需要修改维护其中某个或某几个字段例如只想维护商品价格其余的使用默认值即可使用模板化技术后可能在CSV中就是这样的表现createCommodityParams,createCategoryParams,... { input: { skuList: [ { price: 1, retailPrice: 6 } }, ...或者这样- keyPath: $.input.skuList[0].price value: 1 - keyPath: $.input.skuList[0].retailPrice value: 6亦或使用Mustache将需要修改的value进行参数化{{value}}。我们可以看到这样处理后的数据驱动的文件就变得简洁清晰的许多当一个文件中维护了多个用例且入参字段很多时这样维护起来就可以清晰的看出每个数据对应的用例的作用了price就是为了测试价格的stock就是为了测试库存的publish就是为了测试上下架的等等。注 当然此层的使用视实际情况而定有可能这个接口的参数本身就没多少那么直接全量使用就行或者你就是觉得数据量哪怕再大我都能分得清楚看的明白不用也rawData是可以的~6、Base此层主要放置我们需要处理的公共前置条件和一些自动化公共方法也可以理解为公共的config和util。在我们实际的自动化开发过程中有很多前置条件或公共方法比如登录处理log 处理断言方法或一些数据处理使用过程中所有的service和testcase层都会继承此类这样这些公共方法和前置条件便可直接通用在各个业务线之间也可保持一致性。三、完结最后我们来看下整体分层后的目录结构总览└─apiautotest └─project └─rawData原始参数 ├─testRawData.json └─service用例服务 └─apiObject单接口预处理单接口入参的构造接口的请求与响应值返回 ├─testApiObject.py └─caseService多接口预处理测试步骤teststep或场景的有序集合 ├─testCaseService.py └─util工具类 ├─util.py └─testcase测试用例 └─testDataDriven测试数据驱动 ├─testData.csv ├─testcase.py测试用例集 └─testBase.py测试基类初始化和公共方法 └─platformapiApi定义 ├─testApiDefinition.py最后感谢每一个认真阅读我文章的人礼尚往来总是要有的虽然不是什么很值钱的东西如果你用得到的话可以直接拿走这些资料对于做【软件测试】的朋友来说应该是最全面最完整的备战仓库这个仓库也陪伴我走过了最艰难的路程希望也能帮助到你凡事要趁早特别是技术行业一定要提升技术功底。