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

- 因?yàn)楹笈_(tái)會(huì)配置3套券,初次領(lǐng)券成功后,只會(huì)發(fā)放其中一套,所以在對(duì)領(lǐng)券接口的出參進(jìn)行基本校驗(yàn)后,還需對(duì)券記錄進(jìn)行詳細(xì)的檢查,就需要使用后置腳本,獲取到券配置后再對(duì)數(shù)據(jù)表進(jìn)行核對(duì),需要校驗(yàn)的表包括業(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)校驗(yàn)
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="當(dāng)前券類型為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="券包信息非空")
#獲取用戶分組,確定用戶是命中了實(shí)驗(yàn)組的
group = user_coupon_info[0].get("group")
asserts.assertNotEqual(group, 0, msg="用戶命中對(duì)照組,無膨脹券")
#獲取膨脹資產(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)惠券表校驗(yàn)
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)券成功后進(jìn)行膨脹。查詢優(yōu)惠側(cè)賬戶表,將查詢結(jié)果作為變量,在下一個(gè)接口的前置腳本中,進(jìn)行券到賬的校驗(yàn);
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 '請(qǐng)傳入查詢條件'
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)膨脹失敗,校驗(yàn)接口code非200,再次核對(duì)券表,校驗(yàn)確實(shí)只到賬了一張券。
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一張")
- 其他類似的場(chǎng)景,可以通過復(fù)制已有的用例或步驟直接使用。


2.2 公共組件的編寫
一些需要重復(fù)調(diào)用的功能,我們可以寫成公共組件,不僅方便自己,也方便他人。- 在編寫組件時(shí),如果有入?yún)?,需要考慮參數(shù)值有可能是局部變量的場(chǎng)景。以下面的組件為例,實(shí)現(xiàn)的功能是通過數(shù)據(jù)庫查詢優(yōu)惠券發(fā)放記錄表,可以針對(duì)用戶ID、優(yōu)惠資產(chǎn)ID進(jìn)行查詢。考慮到這兩個(gè)參數(shù)有可能是局部變量,由于目前公共組件類型的入?yún)⒉恢С?{}參數(shù)類型,所以換一種方式來實(shí)現(xiàn) —— 設(shè)置2個(gè)入?yún)ⅲ粋€(gè)為對(duì)應(yīng)的value,一個(gè)為局部定義的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 '請(qǐng)傳入查詢條件'
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 測(cè)試計(jì)劃的執(zhí)行
配置平臺(tái)用例計(jì)劃,選擇依賴應(yīng)用,按照自己的需要選擇執(zhí)行頻次。然后再編輯計(jì)劃,配置匹配規(guī)則,可以看到關(guān)聯(lián)的自動(dòng)化用例。


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

3平臺(tái)編寫case的常用方法
3.1 查詢DB數(shù)據(jù)庫
- 在環(huán)境變量中配置數(shù)據(jù)庫連接信息
- 在腳本中對(duì)數(shù)據(jù)表進(jìn)行查詢
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 '請(qǐng)傳入查詢條件'
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請(qǐng)求時(shí),host選擇對(duì)應(yīng)的環(huán)境變量即可
3.3 一個(gè)case下多個(gè)隨機(jī)賬號(hào)切換請(qǐng)求
- 隨機(jī)創(chuàng)建用戶后,獲取當(dāng)前登錄信息,將請(qǐ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"))
- 在下一次再次需要使用該賬號(hào)時(shí),替換請(qǐng)求頭即可
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使用平臺(tái)時(shí)遇到的一些問題
4.1 查詢r(jià)edis,返回的數(shù)據(jù)帶b'
解決方法一:不使用平臺(tái)的工具,代碼如下:
import redis
redisConn = redis.Redis(host='redis.host', port=666, password='test123',db=1, decode_respnotallow=True)
解決方法二:redis平臺(tái)工具返回是數(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.會(huì)有error。
4.3 http組件json請(qǐng)求體中有中文,運(yùn)行報(bào)錯(cuò)

解決方式:請(qǐng)求頭配置 application/json;charset=UTF-8

5總結(jié)
接入自動(dòng)化平臺(tái)后,方便了很多,也還有更多的使用場(chǎng)景待探索和交流。自動(dòng)化最主要的目的是提效,時(shí)間節(jié)省下來后我們可以有更多的時(shí)間去思考異常場(chǎng)景以及復(fù)雜場(chǎng)景,做一些探索測(cè)試,減少因?yàn)橛美O(shè)計(jì)遺漏而發(fā)生的問題。