• 干货 | 接口自动化测试分层设计与实践总结


    接口测试三要素

    • 参数构造

    • 发起请求,获取响应

    • 校验结果

    一、原始状态

    当我们的用例没有进行分层设计的时候,只能算是一个“苗条式”的脚本。以一个后台创建商品活动的场景为例,大概流程是这样的(默认已经是登录状态下):

    创建商品-创建分类-创建优惠券-创建活动

    要进行接口测试的话,按照接口测试的三要素来进行,具体的效果如下:

    1. # 1、参数构造
    2. createCommodityParams = {
    3. "input": {
    4. "title": "活动商品",
    5. "subtitle": "",
    6. "brand": "",
    7. "categoryLevel1Code": "12",
    8. "categoryLevel2Code": "1312",
    9. "categoryLevel3Code": "131211",
    10. "detail": [
    11. {
    12. "uri": "ecommerce/1118d9.jpg",
    13. "type": 0
    14. }
    15. ],
    16. "installInfo": {
    17. "installType": 1,
    18. "installFee": null
    19. },
    20. "pictureList": [
    21. {
    22. "uri": "ecommerce/222.jpg",
    23. "main": true
    24. }
    25. ],
    26. "postageInfo": {
    27. "postageType": 2,
    28. "postageFee": 1,
    29. "postageId": null
    30. },
    31. "sellerDefinedCode": "",
    32. "publish": 1,
    33. "skuList": [
    34. {
    35. "skuCode": "",
    36. "externalSkuCode": "",
    37. "price": 1,
    38. "retailPrice": 6,
    39. "stock": 100,
    40. "weight": 0,
    41. "suggestPrice": 0,
    42. "skuAttrValueList": [
    43. {
    44. "attrCode": "COLOR",
    45. "attrName": "颜色",
    46. "attrValue": "绿色",
    47. "attrValueId": "1001"
    48. }
    49. ]
    50. }
    51. ],
    52. "jumpSwitch":false,
    53. "recommendCommodityCodeList": [],
    54. "recommendFittingCodeList": [],
    55. "mallCode": "8h4xxx"
    56. }
    57. }
    58. createCategoryParams = {......}
    59. createCouponParams = {......}
    60. createPublicityParams = {......}
    61. publishCommodityParams = {......}
    62. publishPublicityParams = {......}
    63. createCommodityParams["input"]["title"] = "autoTest" + str(time.time())
    64. createCommodityParams["input"]["mallCode"] = self.mallCode
    65. createCommodityParams["input"]["skuList"][0]["price"] = random.randint(1,10)
    66. createCategoryParams["input"]["categoryName"] = "autoTestCategory" + str(time.time())
    67. createCouponParams。。。
    68. createPublicityParams。。。
    69. publishCommodityParams。。。
    70. publishPublicityParams。。。
    71. # 2、发起请求,获取响应
    72. # 创建商品并获取商品code
    73. createCommodityRes = api.getUrl("testApi.create.commodity").post.params(createCommodityParams)
    74. commodityCode = createCommodityRes["commodityCode"]
    75. # 创建分类并获取分类code
    76. createCategoryRes = api.getUrl("testApi.create.category").post.params(createCategoryParams)
    77. categoryCode = createCategoryRes["categoryCode"]
    78. # 创建优惠券并获取优惠券code
    79. createCouponRes = api.getUrl("testApi.create.coupon").post.params(createCouponParams)
    80. couponCode = createCouponRes["couponCode"]
    81. # 创建活动并关联商品,绑定优惠券,设置分类
    82. createPublicityParams["input"]["commodityCode"] = commodityCode
    83. createPublicityParams["input"]["categoryCode"] = categoryCode
    84. createPublicityParams["input"]["couponCode"] = couponCode
    85. createPublicityRes = api.getUrl("testApi.create.publicity").post.params(createPublicityParams)
    86. # 结果校验(断言)
    87. assert.equal(createPublicityRes["code"], 0)
    88. assert.equal(createPublicityRes["publicityName"], createPublicityParams["publicityName"])
    89. 。。。

    按照上面的写法,对于单个脚本的调式来说或许可以,但是一旦用例的数量和复杂程度积累起来后,其维护成本将是巨大的,或者可以说不具备可维护性。

    弊端说明

    • 可读性差,所有的处理都放在一起,代码量大,不简洁直观

    • 灵活性差,参数写死在脚本,适用用例范围小

    • 复用性差,如果其他用例需要同样或类似的步骤,需要重新写一份

    • 维护性差,如果接口有任何改动,那么所有涉及到此接口的脚本都需要一一修改

    例如:随着用例场景的增加,就可能会出现下面这种情况

    按照原始的模式,我们就需要些3个脚本文件分别来描述着3个场景,并且创建商品_API创建分类_API创建优惠券_API在场景1,2,3中均出现了;上架商品_API在场景2,3中均出现。由此我们完全可以预见到,当几百上千的用例场景出现后,这种形式是没有维护性可言的。

    二、进化历程

    因此我们依照着痛点,以最开始的原始状态为例,对用例进行分层改造,来看看进化后的状态。

    1、API 定义层

    我们编程的时候会将一些重复的代码进行封装使用,那么这里依然可以借用这种思想,我们将 API 的定义单独抽离,单独定义。

    我们期望的效果是这样的:

    提前将API的定义放在一层,供用例场景引用,这样当接口有任何修改时,我们只需要修改API definition层即可。

    实例演示

    对应着上面的demo,我们就是需要做如下抽离:

    class APIDefinition:
    ‘’’
    创建商品API定义
    createCommodityParams: 创建商品接口入参
    return:创建商品接口响应结果
    ‘’’
    def createCommodityRequest(createCommodityParams):
    return api.getUrl(“testApi.create.commodity”).post.params(createCommodityParams)

    1. '''
    2. 创建分类API定义
    3. createCategoryParams: 创建分类接口入参
    4. return:创建分类接口响应结果
    5. '''
    6. def createCategoryRequest(createCategoryParams)
    7. return api.getUrl("testApi.create.category").post.params(createCategoryParams)
    8. # 创建优惠券接口定义
    9. def createCouponRequest(createCouponParams)
    10. return api.getUrl("testApi.create.coupon").post.params(createCouponParams)
    11. # 创建活动接口定义
    12. def createPublicityRequest(createPublicityParams)
    13. return api.getUrl("testApi.create.publicity").post.params(createPublicityParams)
    14. # ...其余省略
    2、Service 层

    上面我们已经将接口的定义抽离出来,解决了 API 重复定义的问题,但是再继续分析会发现有一个问题依然没有解决,就是场景的复用性.

    再看刚才的图:

    3个场景中都有重复的步骤,类似创建商品创建分类创建优惠券这些,并且这些步骤都是一个个API的组合,一个步骤对应一个API,在各个步骤之间还会有数据的处理与传递,为了解决这些问题,将对场景再次做抽离,这里我称之为 service 层。

    这一层之所以叫做service(服务)层,是因为它的作用是用来提供测试用例所需要的各种“服务”,好比参数构建、接口请求、数据处理、测试步骤。

    用下图先来看分层的目标:

    我们希望将常用的测试场景步骤封装至service层中,供用例场景调用,增加复用性,也可以理解为测试用例的前置处理;

    但是这里还是有一点小问题,就是service层的东西太多太杂,有些场景步骤可能只适用于我当前的项目用例,在实际的工作中,各个系统间是相互依赖的,前台APP的测试很大可能就依赖后台创建作为前置条件

    好比我在APP端只要商品和分类,可能只想创建商品和分类,并不想创建优惠券,这个时候service层就没有适用的场景步骤供调用,那么我就需要根据自己的需要重新封装;可是对于很多单接口的前置数据处理又是一致的,比如:

    1. createCommodityParams["input"]["title"] = "autoTest" + str(time.time())
    2. createCommodityParams["input"]["mallCode"] = self.mallCode
    3. createCommodityParams["input"]["skuList"][0]["price"] = random.randint(1,10)
    4. createCategoryParams["input"]["categoryName"] = "autoTestCategory" + str(time.time())
    5. createCouponParams。。。
    6. createPublicityParams。。。
    7. publishCommodityParams。。。
    8. publishPublicityParams。。。

    重新封装的话还要再处理这一步,就有点麻烦且不符合我们的复用性设计了,因此我们对service层再细化为3层,分别为:

    apiObject

    单接口的预处理层,这一层主要作用是单接口入参的构造,接口的请求与响应值返回

    • 每个接口请求不依赖与业务步骤,都是单接口的请求;

    • 此外一些简单固定的入参构建也直接放在这里处理,比如随机的商品名,title等,和具体业务流程无关,针对所有调用此接口的场景均适用

    caseService

    多接口的预处理层,这一层主要是测试步骤(teststep)或场景的有序集合。

    • 用例所需要的步骤,通过每一个请求进行组合,每一个步骤都对应着一个API请求,这些步骤会组成一个个场景,各个场景之间可以互相调用组成新的场景,以适应不同的测试用例需求。

    • 场景封装好以后可以供不同的测试用例调用,除了当前项目的用例,其他业务线需要的话也可从此caseService中选择调用,提高复用性的同时也避免了用例相互依赖的问题。

    util

    这一层主要放置针对当前业务的接口需要处理的数据

    • 在实际编写测试步骤时,可能部分接口的参数是通过其他接口获取后经过处理才可以使用,或是修改数据格式,或是修改字段名称,亦或是某些 value 的加解密处理等。

    细化分层后,各层的职责便更加清晰明确,具体如下图:

    实例演示

    apiObject:

    1. class ApiObject:
    2. def createCommodity(createCommodityParams):
    3. inputParams = ApiParamsBuild().createCommodityParamsBuild(createCommodityParams)
    4. response = APIDefinition().createCommodityRequest(inputParams)
    5. return response
    6. def createCategory(createCategoryParams):
    7. ...
    8. def createCoupon(createCouponParams):
    9. ...
    10. ......
    11. class ApiParamsBuild:
    12. def createCommodityParamsBuild(createCommodityParams):
    13. createCommodityParams["input"]["title"] = "autoTest" + str(time.time())
    14. createCommodityParams["input"]["mallCode"] = self.mallCode
    15. createCommodityParams["input"]["skuList"][0]["price"] = random.randint(1,10)
    16. return createCommodityParams
    17. def createCategoryParamsBuild(createCategoryParams):
    18. ...
    19. def createCouponParamsBuild(createCouponParams):
    20. ...
    21. ......

    到此,我们来看看原始的用例经过目前封装后的模样:

    1、参数构造

    1. createCommodityParams = {
    2. "input": {
    3. "title": "活动商品",
    4. "subtitle": "",
    5. "brand": "",
    6. "categoryLevel1Code": "12",
    7. "categoryLevel2Code": "1312",
    8. "categoryLevel3Code": "131211",
    9. "detail": [
    10. {
    11. "uri": "ecommerce/1118d9.jpg",
    12. "type": 0
    13. }
    14. ],
    15. "installInfo": {
    16. "installType": 1,
    17. "installFee": null
    18. },
    19. "pictureList": [
    20. {
    21. "uri": "ecommerce/222.jpg",
    22. "main": true
    23. }
    24. ],
    25. "postageInfo": {
    26. "postageType": 2,
    27. "postageFee": 1,
    28. "postageId": null
    29. },
    30. "sellerDefinedCode": "",
    31. "publish": 1,
    32. "skuList": [
    33. {
    34. "skuCode": "",
    35. "externalSkuCode": "",
    36. "price": 1,
    37. "retailPrice": 6,
    38. "stock": 100,
    39. "weight": 0,
    40. "suggestPrice": 0,
    41. "skuAttrValueList": [
    42. {
    43. "attrCode": "COLOR",
    44. "attrName": "颜色",
    45. "attrValue": "绿色",
    46. "attrValueId": "1001"
    47. }
    48. ]
    49. }
    50. ],
    51. "jumpSwitch":false,
    52. "recommendCommodityCodeList": [],
    53. "recommendFittingCodeList": [],
    54. "mallCode": "8h4xxx"
    55. }
    56. }
    57. createCategoryParams = {......}
    58. createCouponParams = {......}
    59. createPublicityParams = {......}
    60. publishCommodityParams = {......}
    61. publishPublicityParams = {......}
    62. # 2、发起请求,获取响应
    63. # 创建商品并获取商品code
    64. createCommodityRes = ApiObject().createCommodity(createCommodityParams)
    65. commodityCode = createCommodityRes["commodityCode"]
    66. # 创建分类并获取分类code
    67. createCategoryRes = ApiObject().createCategory(createCategoryParams)
    68. categoryCode = createCategoryRes["categoryCode"]
    69. # 创建优惠券并获取优惠券code
    70. createCouponRes = ApiObject().createCoupon(createCouponParams)
    71. couponCode = createCouponRes["couponCode"]
    72. # 创建活动并关联商品,绑定优惠券,设置分类
    73. createPublicityParams["input"]["commodityCode"] = commodityCode
    74. createPublicityParams["input"]["categoryCode"] = categoryCode
    75. createPublicityParams["input"]["couponCode"] = couponCode
    76. createPublicityRes = ApiObject().createPublicity(createPublicityParams)
    77. # 结果校验(断言)
    78. assert.equal(createPublicityRes["code"], 0)
    79. assert.equal(createPublicityRes["publicityName"], createPublicityParams["publicityName"])
    80. 。。。
    1. 可以看到,现在接口请求的url、method、通用入参处理等已经不会在用例中体现了,接下来继续封装caseService层。
    2. caseService:
    3. 我们将多接口的场景步骤进行封装
    4. ``` class CaseService:
    5. def createPublicityByCategory(params):
    6. # 创建商品并获取商品code
    7. createCommodityRes = ApiObject().createCommodity(createCommodityParams)
    8. commodityCode = createCommodityRes["commodityCode"]
    9. # 创建分类并获取分类code
    10. createCategoryRes = ApiObject().createCategory(createCategoryParams)
    11. categoryCode = createCategoryRes["categoryCode"]
    12. # 创建优惠券并获取优惠券code
    13. createCouponRes = ApiObject().createCoupon(createCouponParams)
    14. couponCode = createCouponRes["couponCode"]
    15. # 创建活动并关联商品,绑定优惠券,设置分类
    16. createPublicityParams["input"]["commodityCode"] = commodityCode
    17. createPublicityParams["input"]["categoryCode"] = categoryCode
    18. createPublicityParams["input"]["couponCode"] = couponCode
    19. createPublicityRes = ApiObject().createPublicity(createPublicityParams)
    20. return createPublicityRes
    21. ......

    这时体现在用例中的表现就如下层testcase层所示.

    3、testcase 层
    我们想要的是一个清晰明了,“一劳永逸”的自动化测试用例,就像我们的手工测试用例一样,我们的前置条件可以复用,我们入参可以任意修改,但测试步骤都是固定不变的(前提可能是产品没有偷偷改需求~)。

    这一层其实是对应的testsuite(测试用例集),是测试用例的无序集合。其中各个用例之间应该是相互独立,互不干扰,不存在依赖关系,每个用例都可以单独运行。

    最终我们期望自动化用例的维护过程中达到的效果如下:

    testcase 层:

    1. # 1、参数构造
    2. createCommodityParams = {
    3. "input": {
    4. "title": "活动商品",
    5. "subtitle": "",
    6. "brand": "",
    7. "categoryLevel1Code": "12",
    8. "categoryLevel2Code": "1312",
    9. "categoryLevel3Code": "131211",
    10. "detail": [
    11. {
    12. "uri": "ecommerce/1118d9.jpg",
    13. "type": 0
    14. }
    15. ],
    16. "installInfo": {
    17. "installType": 1,
    18. "installFee": null
    19. },
    20. "pictureList": [
    21. {
    22. "uri": "ecommerce/222.jpg",
    23. "main": true
    24. }
    25. ],
    26. "postageInfo": {
    27. "postageType": 2,
    28. "postageFee": 1,
    29. "postageId": null
    30. },
    31. "sellerDefinedCode": "",
    32. "publish": 1,
    33. "skuList": [
    34. {
    35. "skuCode": "",
    36. "externalSkuCode": "",
    37. "price": 1,
    38. "retailPrice": 6,
    39. "stock": 100,
    40. "weight": 0,
    41. "suggestPrice": 0,
    42. "skuAttrValueList": [
    43. {
    44. "attrCode": "COLOR",
    45. "attrName": "颜色",
    46. "attrValue": "绿色",
    47. "attrValueId": "1001"
    48. }
    49. ]
    50. }
    51. ],
    52. "jumpSwitch":false,
    53. "recommendCommodityCodeList": [],
    54. "recommendFittingCodeList": [],
    55. "mallCode": "8h4xxx"
    56. }
    57. }
    58. createCategoryParams = {......}
    59. createCouponParams = {......}
    60. createPublicityParams = {......}
    61. publishCommodityParams = {......}
    62. publishPublicityParams = {......}
    63. # 2、发起请求,获取响应
    64. createPublicityRes = CaseService().createPublicityByCategory(createCommodityParams,createCategoryParams,createCouponParams...)
    65. # 结果校验(断言)
    66. assert.equal(createPublicityRes["code"], 0)
    67. assert.equal(createPublicityRes["publicityName"], createPublicityParams["publicityName"])
    68. 。。。

    可以看到,这时涉及到用例场景步骤的代码已经非常少了,并且完全独立,与框架、其他用例等均无耦合。

    到这里我们再看用例,会发现一点,测试数据依然冗长,那么下面就开始对测试数据进行参数化和数据驱动的处理。

    4、testdata
    此层用来管理测试数据,作为参数化场景的数据驱动。

    参数化: 所谓参数化,简单来说就是将入参利用变量的形式传入,不要将参数写死,增加灵活性,好比搜索商品的接口,不同的关键字和搜索范围作为入参,就会得到不同的搜索结果。上面的例子中其实已经是参数化了。

    数据驱动:对于参数,我们可以将其放入一个文件中,可以存放多个入参,形成一个参数列表的形式,然后从中读取参数传入接口即可。常见做数据驱动的有 JSON、CSV、YAML 等。

    实例演示

    我们以CSV为例,不特别依照某个框架,通常测试框架都具备参数化的功能。

    将所需要的入参放入test.csv文件中:

    1. createCommodityParams,createCategoryParams,...
    2. {
    3. "input": {
    4. "title": "活动商品",
    5. "subtitle": "",
    6. "brand": "",
    7. "categoryLevel1Code": "12",
    8. "categoryLevel2Code": "1312",
    9. "categoryLevel3Code": "131211",
    10. "detail": [
    11. {
    12. "uri": "ecommerce/1118d9.jpg",
    13. "type": 0
    14. }
    15. ],
    16. "installInfo": {
    17. "installType": 1,
    18. "installFee": null
    19. },
    20. "pictureList": [
    21. {
    22. "uri": "ecommerce/222.jpg",
    23. "main": true
    24. }
    25. ],
    26. "postageInfo": {
    27. "postageType": 2,
    28. "postageFee": 1,
    29. "postageId": null
    30. },
    31. "sellerDefinedCode": "",
    32. "publish": 1,
    33. "skuList": [
    34. {
    35. "skuCode": "",
    36. "externalSkuCode": "",
    37. "price": 1,
    38. "retailPrice": 6,
    39. "stock": 100,
    40. "weight": 0,
    41. "suggestPrice": 0,
    42. "skuAttrValueList": [
    43. {
    44. "attrCode": "COLOR",
    45. "attrName": "颜色",
    46. "attrValue": "绿色",
    47. "attrValueId": "1001"
    48. }
    49. ]
    50. }
    51. ],
    52. "jumpSwitch":false,
    53. "recommendCommodityCodeList": [],
    54. "recommendFittingCodeList": [],
    55. "mallCode": "8h4xxx"
    56. }
    57. },
    58. ...

    然后再回到用例层,利用框架参数化的功能对数据进行读取

    1. # 1、参数构造
    2. @parametrize(params = readCsv("test.csv"))
    3. # 2、发起请求,获取响应
    4. createPublicityRes = CaseService().createPublicityByCategory(params)
    5. # 结果校验(断言)
    6. assert.equal(createPublicityRes["code"], 0)
    7. assert.equal(createPublicityRes["publicityName"], createPublicityParams["publicityName"])
    8. 。。。

    注:这里的测试数据,不仅仅局限于接口的请求参数,既然做数据驱动,那么断言也可以维护在此,以减少用例层的代码冗余。

    5、rawData
    这一层是存放接口原始入参的地方。

    某些接口的入参可能很多,其中很多参数值又可能是固定不变的,构建入参的时候我们只想对"变"的值进行动态的维护,而不维护的值就使用原始参数中的默认值,以此减少工作量(emmm…可能也就是CV大法的量吧~)

    再者就是数据驱动的数据文件中只维护需要修改的参数,使数据文件更简洁,可阅读性更强。

    实例演示:

    这种利用原始参数(rawData)的方法我们称之为模板化,实际工作中有多种方式可实现,例如jsonpath、Mustache或者自己根据需求实现方法,本文重点在介绍分层设计,所以就不具体演示模板化技术的细节了,仅说明设计此层的作用。

    以实例中的入参createCommodityParams为例,未用模板化技术前,我们要在CSV里面维护完整的入参:

    1. createCommodityParams,createCategoryParams,...
    2. {
    3. "input": {
    4. "title": "活动商品",
    5. "subtitle": "",
    6. "brand": "",
    7. "categoryLevel1Code": "12",
    8. "categoryLevel2Code": "1312",
    9. "categoryLevel3Code": "131211",
    10. "detail": [
    11. {
    12. "uri": "ecommerce/1118d9.jpg",
    13. "type": 0
    14. }
    15. ],
    16. "installInfo": {
    17. "installType": 1,
    18. "installFee": null
    19. },
    20. "pictureList": [
    21. {
    22. "uri": "ecommerce/222.jpg",
    23. "main": true
    24. }
    25. ],
    26. "postageInfo": {
    27. "postageType": 2,
    28. "postageFee": 1,
    29. "postageId": null
    30. },
    31. "sellerDefinedCode": "",
    32. "publish": 1,
    33. "skuList": [
    34. {
    35. "skuCode": "",
    36. "externalSkuCode": "",
    37. "price": 1,
    38. "retailPrice": 6,
    39. "stock": 100,
    40. "weight": 0,
    41. "suggestPrice": 0,
    42. "skuAttrValueList": [
    43. {
    44. "attrCode": "COLOR",
    45. "attrName": "颜色",
    46. "attrValue": "绿色",
    47. "attrValueId": "1001"
    48. }
    49. ]
    50. }
    51. ],
    52. "jumpSwitch":false,
    53. "recommendCommodityCodeList": [],
    54. "recommendFittingCodeList": [],
    55. "mallCode": "8h4xxx"
    56. }
    57. },
    58. ...

    但是实际上,我们可能仅仅需要修改维护其中某个或某几个字段(例如只想维护商品价格),其余的使用默认值即可,使用模板化技术后可能在CSV中就是这样的表现:

    1. createCommodityParams,createCategoryParams,...
    2. {
    3. "input": {
    4. "skuList": [
    5. {
    6. "price": 1,
    7. "retailPrice": 6
    8. }
    9. },
    10. ...

    或者这样

    1. - keyPath: $.input.skuList[0].price
    2. value: 1
    3. - keyPath: $.input.skuList[0].retailPrice
    4. value: 6

    亦或使用Mustache,将需要修改的value进行参数化{{value}}。

    我们可以看到,这样处理后的数据驱动的文件就变得简洁清晰的许多,当一个文件中维护了多个用例且入参字段很多时,这样维护起来就可以清晰的看出每个数据对应的用例的作用了;

    price就是为了测试价格的,stock就是为了测试库存的,publish就是为了测试上下架的等等。

    注: 当然,此层的使用视实际情况而定,有可能这个接口的参数本身就没多少,那么直接全量使用就行,或者你就是觉得数据量哪怕再大我都能分得清楚,看的明白,不用也rawData是可以的~

    6、Base
    此层主要放置我们需要处理的公共前置条件和一些自动化公共方法,也可以理解为公共的config和util。

    在我们实际的自动化开发过程中,有很多前置条件或公共方法,比如登录处理,log 处理,断言方法或一些数据处理;

    使用过程中所有的service和testcase层都会继承此类,这样这些公共方法和前置条件便可直接通用;在各个业务线之间也可保持一致性。

    三、完结
    最后,我们来看下整体分层后的目录结构总览:

    1. └─apiautotest
    2. └─project
    3. └─rawData(原始参数)
    4. ├─testRawData.json
    5. └─service(用例服务)
    6. └─apiObject(单接口预处理,单接口入参的构造,接口的请求与响应值返回)
    7. ├─testApiObject.py
    8. └─caseService(多接口预处理,测试步骤(teststep)或场景的有序集合)
    9. ├─testCaseService.py
    10. └─util(工具类)
    11. ├─util.py
    12. └─testcase(测试用例)
    13. └─testDataDriven(测试数据驱动)
    14. ├─testData.csv
    15. ├─testcase.py(测试用例集)
    16. └─testBase.py(测试基类,初始化和公共方法)
    17. └─platformapi(Api定义)
    18. ├─testApiDefinition.py

    最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

    这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你! 

  • 相关阅读:
    centos7编译安装qt4遇到的问题
    pytorch固定随机数中种子
    wordpress博客趣主题个人静态网页模板
    C语言——流程控制
    南王科技IPO过会:年营收12亿 华莱士关联方持有近28%股权
    JavaScript 69 JavaScript Web API 69.1 Web API - 简介
    在Windows下Edge浏览器OA发起流程问题
    vue axios跨域
    Saas型网站域名如何接入腾讯云的web应用防火墙?
    河南短视频代运营|短视频代运营能不能纯佣金合作
  • 原文地址:https://blog.csdn.net/2301_78276982/article/details/134297438