軟件測試 | 接口自動化測試分層設計與實踐總結


在這里插入圖片描述

本文為霍格沃茲測試學院優秀學員@月關同學的學習時間總結,進階學習文末加群!
本文以筆者當前使用的自動化測試項目為例,淺談分層設計的思路,不涉及到具體的代碼細節和某個框架的實現原理,重點關注在分層前后的使用對比,可能會以一些偽代碼為例來說明舉例。

更多技術文章分享及測試資料點此獲取

接口測試三要素

  • 參數構造
  • 發起請求,獲取響應
  • 校驗結果

一、原始狀態

當我們的用例沒有進行分層設計的時候,只能算是一個“苗條式”的腳本。以一個后台創建商品活動的場景為例,大概流程是這樣的(默認已經是登錄狀態下):

創建商品-創建分類-創建優惠券-創建活動

要進行接口測試的話,按照接口測試的三要素來進行,具體的效果如下:

# 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在場景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)

 '''
 創建分類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(測試基類,初始化和公共方法) 
  └─platformapi(Api定義)
   ├─testApiDefinition.py

以上,期待與各位同學一起交流探討。

更多技術文章分享及測試資料點此獲取


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM