
本文為霍格沃茲測試學院優秀學員@月關同學的學習時間總結,進階學習文末加群!
本文以筆者當前使用的自動化測試項目為例,淺談分層設計的思路,不涉及到具體的代碼細節和某個框架的實現原理,重點關注在分層前后的使用對比,可能會以一些偽代碼為例來說明舉例。
更多技術文章分享及測試資料點此獲取
接口測試三要素:
- 參數構造
- 發起請求,獲取響應
- 校驗結果
一、原始狀態
當我們的用例沒有進行分層設計的時候,只能算是一個“苗條式”的腳本。以一個后台創建商品活動的場景為例,大概流程是這樣的(默認已經是登錄狀態下):
創建商品-創建分類-創建優惠券-創建活動
要進行接口測試的話,按照接口測試的三要素來進行,具體的效果如下:
# 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
以上,期待與各位同學一起交流探討。
