0前言
相信大家對接口自動化已經(jīng)不陌生了,這是幾乎我們每個迭代都會投入的事情,但耗費了這么多精力去編寫和維護,實際的收益如何呢?如果收益不好,是不是說明我們自動化case的實現(xiàn)方式、使用方式還有改進的地方呢?以下是接入得物接口自動化平臺后的一些實踐和想法,歡迎大家積極交流~
1淺談接口自動化
1.1 使用場景&可以帶來的效果
- 給開發(fā)用 - 提高自測效率&提測質(zhì)量
在接入自動化平臺前,我們只能本地拉取代碼->執(zhí)行用例,所以執(zhí)行者也只有測試人員。接入平臺后,通過宣導(dǎo)or分享,開發(fā)可以方便的找到需要的用例(用例模塊和標題需描述清晰),從而幫助他們造數(shù)或自測。
對于一些核心場景,即使業(yè)務(wù)迭代,通常結(jié)果也不會發(fā)生太大變化,這一類的場景case如果設(shè)計地較為穩(wěn)定(當然這里的穩(wěn)定不是只校驗code=200就行),可以分享給開發(fā)用于自測,根據(jù)開發(fā)同學(xué)使用后的反饋,他們自測簡單了許多,也有幫助他們發(fā)現(xiàn)過問題。
另外有一些本迭代內(nèi)的新增接口,在接口評審?fù)瓿珊螅覀兛梢蕴崆熬帉懞?,根?jù)具體情況決定是先保證接口狀態(tài)的正常,后續(xù)再補充數(shù)據(jù)邏輯的校驗,還是直接先把case寫好。因為很多時候開發(fā)自測都只是調(diào)用本地代碼,提測后連接口都調(diào)不通,如果提測前可以先進行基本的校驗,就能減少冒煙測試被阻塞的概率。
冒煙測試:針對改動點挑出涉及的接口case,再加上P0級別case,提測后先執(zhí)行一遍看看是否正常,如果核心鏈路異常,阻塞了后續(xù)測試,就可以直接打回了。
驗證bug:有些復(fù)雜場景,測試鏈路較長,測試數(shù)據(jù)準備又很困難,很容易出現(xiàn)bug,而出現(xiàn)bug也就算了,偏偏改一遍還不一定能改好...這時候自動化的價值就體現(xiàn)了,把這些場景利用自動化實現(xiàn),驗證bug時直接一鍵執(zhí)行就能得出結(jié)果,大大節(jié)省了時間,同時也穩(wěn)定了自己瀕臨暴躁的情緒。
回歸測試:在每次的bvt測試、覆蓋率跟進中,有些case可能并不涉及本次需求改動范圍,場景又比較簡單基礎(chǔ),我們就可以利用自動化去覆蓋。執(zhí)行通過,視具體情況可以簡單看一眼或者不再回歸。
雖然我們現(xiàn)在有了造數(shù)平臺,但實現(xiàn)起來有一定的成本,一些場景可能除了自己沒有別的業(yè)務(wù)方有造數(shù)需求,并且場景很簡單,只需調(diào)個接口,改個數(shù)據(jù)表就行,那么最快的造數(shù)方法就是自動化腳本?,F(xiàn)在有了自動化平臺,我們可以更好地分享給有造數(shù)需求的開發(fā)、產(chǎn)品、測試。
當然,以上效果的前提是我們的自動化case比較穩(wěn)定,不能每次執(zhí)行都一堆不通過,這樣時間都耗費在排查問題上了,效果會大打折扣,別人也不會再愿意使用。
1.2 什么時間去寫自動化case
通常一部分同學(xué)會在用例評審結(jié)束,開發(fā)提測之前進行case編寫,此時需要實現(xiàn)自動化的場景已經(jīng)明確,基本上涉及的接口和出入?yún)⒍家汛_定,自動化case的大致框架就形成了。這時候?qū)崿F(xiàn)自動化,就可以最大化地發(fā)揮其價值,在上述涉及到的幾個場景都能投入使用。如果因為時間不夠或接口尚未明確,可以先梳理好需要實現(xiàn)自動化的場景步驟,在提測后一邊手動執(zhí)行用例一邊補充接口參數(shù)和校驗點。針對級別較低的接口場景,也可以放在版本結(jié)束后再實現(xiàn),只是效果會降低一些。
1.3 自動化維護成本太高怎么辦
我們維護的case一般有兩種,一是自己寫的,二是別人寫的。自己寫的,含著淚也要日常維護。別人寫的,由于大家的編碼風格千差萬別,在接入自動化平臺前,維護起來簡直困難重重,當我們?yōu)榱送ㄟ^率去推進case更新時,往往這一類的難以推進?,F(xiàn)在接入了平臺,基本上統(tǒng)一了case模板,當因為需求變動需要更新時,有時只需要修改出入?yún)⒑蛿嘌约纯?,一定程度上已?jīng)降低了維護成本。
另外,當case經(jīng)常報錯時,可以看看設(shè)計上是否能優(yōu)化。有些依賴性強的數(shù)據(jù),是否可以通過其他手段讓這部分數(shù)據(jù)穩(wěn)定下來。比如發(fā)優(yōu)惠券的場景,前提需要一張有效的券,那我們在發(fā)券前可以先獲取一張有效的券信息,或者在發(fā)券前先創(chuàng)建一張券,發(fā)完券后如果需要對券信息進行校驗,也通過變量的方式。針對單個測試點實現(xiàn)自動化時,可以盡可能地與其他測試點解藕,充分利用前置腳本,通過修改數(shù)據(jù)表等方式較少依賴。case中也可以設(shè)置失敗重試次數(shù),減少由于環(huán)境不穩(wěn)定等原因造成的失敗。
2在自動化平臺上的實踐
2.1 場景case的編寫
舉個例子:“得物App新客人群領(lǐng)取優(yōu)惠券并觸發(fā)金額膨脹,多次觸發(fā)膨脹應(yīng)該只有一次膨脹成功”。
這個case在迭代中提高了測試效率,并且在后續(xù)需求變更時,幫助開發(fā)自測,解決造數(shù)問題并發(fā)現(xiàn)了bug。
- 由于業(yè)務(wù)特性,只有命中實驗組的新用戶才可領(lǐng)券。那么首先需要創(chuàng)建一個新用戶,并添加到ab白名單。然后在領(lǐng)券前先對領(lǐng)券狀態(tài)、用戶身份進行校驗;

- 因為后臺會配置3套券,初次領(lǐng)券成功后,只會發(fā)放其中一套,所以在對領(lǐng)券接口的出參進行基本校驗后,還需對券記錄進行詳細的檢查,就需要使用后置腳本,獲取到券配置后再對數(shù)據(jù)表進行核對,需要校驗的表包括業(yè)務(wù)本身的領(lǐng)券記錄表和優(yōu)惠業(yè)務(wù)側(cè)的賬戶表;
import json
import requests
from util.db_mysql import DBMySQL
from util.db_redis import DbRedis
def call(env_vars, g_vars, l_vars, sys_funcs, asserts, logger, **kwargs):
userId = l_vars.get('userId')
n = int(userId)%4
dbA = DBMySQL(env_vars.get("db.A"))
dbB = DBMySQL(env_vars.get("db.B"))
try:
sql_1 = "SELECT * FROM table_A WHERE user_id = %s;"%userId
# 領(lǐng)券后,用戶領(lǐng)券狀態(tài)校驗
user_coupon_info = dbA.select(sql_1)
logger.info(newbie_res)
asserts.assertEqual(user_coupon_info[0].get("status"), 1, msg="數(shù)據(jù)表領(lǐng)券狀態(tài)為true")
asserts.assertEqual(user_coupon_info[0].get("type"), 0, msg="當前券類型為0")
asserts.assertIsEmpty(user_coupon_info[0].get("coupon1"), msg="無資產(chǎn)1")
asserts.assertIsEmpty(user_coupon_info[0].get("coupon2"), msg="無資產(chǎn)2")
asserts.assertIsEmpty(user_coupon_info[0].get("coupon4"), msg="無資產(chǎn)4")
asserts.assertIsNotEmpty(user_coupon_info[0].get("info"), msg="券包信息非空")
#獲取用戶分組,確定用戶是命中了實驗組的
group = user_coupon_info[0].get("group")
asserts.assertNotEqual(group, 0, msg="用戶命中對照組,無膨脹券")
#獲取膨脹資產(chǎn)配置
sql_2 = "SELECT * FROM table_B WHERE id = 50%s and deleted=0"%group
logger.info("sql_2:"+sql_2)
coupon_config = dbA.select(sql_2)
logger.info("coupon_config:"+coupon_config)
content = json.loads(coupon_config[0].get("content_info"))
for i in range(3):
activityId = content[i]["activityId"]
l_vars.set('activityId_{}'.format(i+1), activityId)
# 優(yōu)惠券表校驗
sql_3 = "SELECT * FROM a_coupon_%s WHERE user_id = %s and activity_id = %s;"%(n,userId,activityId)
logger.info("sql_3:"+sql_3)
coupon_res = dbB.select(sql_3)
logger.info("coupon_res:"+coupon_res)
if(i==0):
asserts.assertIsEmpty(coupon_res, msg="未到賬資產(chǎn)1")
if(i==2):
asserts.assertIsNotEmpty(coupon_res, msg="到賬資產(chǎn)3")
finally:
dbA.close()
dbB.close()
- 領(lǐng)券成功后進行膨脹。查詢優(yōu)惠側(cè)賬戶表,將查詢結(jié)果作為變量,在下一個接口的前置腳本中,進行券到賬的校驗;
import json
import requests
from util.db_mysql import DBMySQL
from util.db_redis import DbRedis
def call(env_vars, g_vars, l_vars, sys_funcs, asserts, logger, **kwargs):
call_param = sys_funcs.get_call_param()
userId = call_param.get('userId')
activityId = call_param.get('activityId')
dbB = DBMySQL(env_vars.get("db.B"))
if not userId:
user_var = l_vars.get(call_param.get('var_userId'))
userId = user_var
if not activityId:
activityId_var = l_vars.get(call_param.get('var_activityId'))
activityId = activityId_var
if not userId and not activityId:
raise '請傳入查詢條件'
try:
if not activityId:
sql = "SELECT * FROM a_coupon_%s WHERE user_id = %s;"%(int(userId)%4,userId)
elif not userId:
sql = "SELECT * FROM a_coupon_%s WHERE activity_id = %s;"%(n,activityId)
else:
sql = "SELECT * FROM a_coupon_%s WHERE user_id = %s and activity_id = %s;"%(int(userId)%4,userId,activityId)
logger.info(sql)
res = dbB.select(sql)
logger.info(res)
l_vars.set("select_tableB_res",res)
except Exception as e:
logger.info(f'查詢失敗【{str(e)}】')
raise e
finally:
dbB.close()
return res
import json
import requests
def call(env_vars, g_vars, l_vars, sys_funcs, asserts, logger, **kwargs):
select_tableB_res = l_vars.get('select_tableB_res')
asserts.assertIsNotEmpty(select_tableB_res, msg="到賬資產(chǎn)1")
- 再次膨脹,應(yīng)膨脹失敗,校驗接口code非200,再次核對券表,校驗確實只到賬了一張券。
import json
import requests
def call(env_vars, g_vars, l_vars, sys_funcs, asserts, logger, **kwargs):
select_tableB_res = l_vars.get('select_tableB_res')
asserts.assertEqual(len(select_tableB_res),1,msg="只到賬資產(chǎn)1一張")
- 其他類似的場景,可以通過復(fù)制已有的用例或步驟直接使用。


2.2 公共組件的編寫
一些需要重復(fù)調(diào)用的功能,我們可以寫成公共組件,不僅方便自己,也方便他人。- 在編寫組件時,如果有入?yún)?,需要考慮參數(shù)值有可能是局部變量的場景。以下面的組件為例,實現(xiàn)的功能是通過數(shù)據(jù)庫查詢優(yōu)惠券發(fā)放記錄表,可以針對用戶ID、優(yōu)惠資產(chǎn)ID進行查詢??紤]到這兩個參數(shù)有可能是局部變量,由于目前公共組件類型的入?yún)⒉恢С?{}參數(shù)類型,所以換一種方式來實現(xiàn) —— 設(shè)置2個入?yún)?,一個為對應(yīng)的value,一個為局部定義的key。腳本中,如果value未獲取到,則去變量空間中獲取局部變量。
拿到查詢結(jié)果后也要盡可能的把結(jié)果存到變量空間,以供后續(xù)步驟的使用。
import json
import requests
from util.db_mysql import DBMySQL
from util.db_redis import DbRedis
def call(env_vars, g_vars, l_vars, sys_funcs, asserts, logger, **kwargs):
call_param = sys_funcs.get_call_param()
userId = call_param.get('userId')
activityId = call_param.get('activityId')
dbA = DBMySQL(env_vars.get("db.A"))
if not userId:
user_var = l_vars.get(call_param.get('var_userId'))
userId = user_var
if not activityId:
activityId_var = l_vars.get(call_param.get('var_activityId'))
activityId = activityId_var
if not userId and not activityId:
raise '請傳入查詢條件'
try:
if not activityId:
sql = "SELECT * FROM a_coupon_%s WHERE user_id = %s;"%(int(userId)%4,userId)
elif not userId:
sql = "SELECT * FROM a_coupon_%s WHERE activity_id = %s;"%(n,activityId)
else:
sql = "SELECT * FROM a_coupon_%s WHERE user_id = %s and activity_id = %s;"%(int(userId)%4,userId,activityId)
logger.info(sql)
res = dbA.select(sql)
logger.info(res)
l_vars.set("select_tableA_res",res)
except Exception as e:
logger.info(f'查詢失敗【{str(e)}】')
raise e
finally:
dbA.close()
return res
2.3 測試計劃的執(zhí)行
配置平臺用例計劃,選擇依賴應(yīng)用,按照自己的需要選擇執(zhí)行頻次。然后再編輯計劃,配置匹配規(guī)則,可以看到關(guān)聯(lián)的自動化用例。


在用例平臺綁定自動化case,在轉(zhuǎn)測單平臺添加自動化計劃,已關(guān)聯(lián)的用例在執(zhí)行結(jié)束后會自動更新執(zhí)行狀態(tài),提高手動執(zhí)行的效率。

3平臺編寫case的常用方法
3.1 查詢DB數(shù)據(jù)庫
- 在環(huán)境變量中配置數(shù)據(jù)庫連接信息
- 在腳本中對數(shù)據(jù)表進行查詢
import json
import requests
from util.db_mysql import DBMySQL
from util.db_redis import DbRedis
def call(env_vars, g_vars, l_vars, sys_funcs, asserts, logger, **kwargs):
call_param = sys_funcs.get_call_param()
userId = call_param.get('userId')
activityId = call_param.get('activityId')
dbA = DBMySQL(env_vars.get("db.A"))
if not userId:
user_var = l_vars.get(call_param.get('var_userId'))
userId = user_var
if not activityId:
activityId_var = l_vars.get(call_param.get('var_activityId'))
activityId = activityId_var
if not userId and not activityId:
raise '請傳入查詢條件'
try:
if not activityId:
sql = "SELECT * FROM a_coupon_%s WHERE user_id = %s;"%(int(userId)%4,userId)
elif not userId:
sql = "SELECT * FROM a_coupon_%s WHERE activity_id = %s;"%(n,activityId)
else:
sql = "SELECT * FROM a_coupon_%s WHERE user_id = %s and activity_id = %s;"%(int(userId)%4,userId,activityId)
logger.info(sql)
res = dbA.select(sql)
logger.info(res)
l_vars.set("select_tableA_res",res)
except Exception as e:
logger.info(f'查詢失敗【{str(e)}】')
raise e
finally:
dbA.close()
return res
3.2 獲取應(yīng)用ip地址作為host域名
- 配置host環(huán)境變量:http://${sys.container.ip:app_name}:8888,app_name為服務(wù)名
- 調(diào)用公共組件獲取ip,傳入服務(wù)名,返回ip
- http請求時,host選擇對應(yīng)的環(huán)境變量即可
3.3 一個case下多個隨機賬號切換請求
- 隨機創(chuàng)建用戶后,獲取當前登錄信息,將請求頭存到本地變量
import json
import requests
def call(env_vars, g_vars, l_vars, sys_funcs, asserts, logger, **kwargs):
l_vars.set("user1",l_vars.get("sys.public.login.headers"))
import json
import requests
def call(env_vars, g_vars, l_vars, sys_funcs, asserts, logger, **kwargs):
l_vars.set("sys.public.login.headers", l_vars.get("user1"))
4使用平臺時遇到的一些問題
4.1 查詢redis,返回的數(shù)據(jù)帶b'
解決方法一:不使用平臺的工具,代碼如下:
import redis
redisConn = redis.Redis(host='redis.host', port=666, password='test123',db=1, decode_respnotallow=True)
解決方法二:redis平臺工具返回是數(shù)據(jù)是 bytes 類型,需要encoding一下
re = DbRedis.ger_redis(link_info)
test = re.get(test_key)
test_str = test.decode(encoding='utf-8')
key = key+test_str
re.set(key,"aaa")
4.2 update、insert、delete語句執(zhí)行成功,數(shù)據(jù)庫卻未生效
解決方式:需要db.commit() ,select語句不需要該語句
dbA = DBMySQL(db_A)
sql = "INSERT INTO t(name,age) VALUES (%s, %s);"
try:
res = db.insert(sql,['lucy', 18])
db.commit()
finally:
dbA.close()
備注:delete方式,刪除數(shù)據(jù)量是0.會有error。
4.3 http組件json請求體中有中文,運行報錯

解決方式:請求頭配置 application/json;charset=UTF-8

5總結(jié)
接入自動化平臺后,方便了很多,也還有更多的使用場景待探索和交流。自動化最主要的目的是提效,時間節(jié)省下來后我們可以有更多的時間去思考異常場景以及復(fù)雜場景,做一些探索測試,減少因為用例設(shè)計遺漏而發(fā)生的問題。