中國(guó)做國(guó)外的網(wǎng)站廣東疫情最新消息今天又封了
本期作者
項(xiàng)目參與人員:
顧伊凡、陳鈺廣、張又中、楊雨浩、樊執(zhí)政、熊夢(mèng)園、何璇、譚楠
UI自動(dòng)化測(cè)試能夠在一定程度上確保產(chǎn)品質(zhì)量,尤其在降本提效的大背景下,其重要性愈發(fā)凸顯。理想情況下,UI自動(dòng)化測(cè)試不僅能夠能幫我們規(guī)避不少線(xiàn)上問(wèn)題,又能加快產(chǎn)品上線(xiàn)速度。然而現(xiàn)實(shí)卻往往相去甚遠(yuǎn),在多數(shù)情況下,編寫(xiě)測(cè)試腳本的工作量很大,且由于應(yīng)用程序的頻繁迭代,這些腳本很快就會(huì)過(guò)時(shí)。此外,網(wǎng)絡(luò)數(shù)據(jù)的多變也常常導(dǎo)致測(cè)試結(jié)果不穩(wěn)定,從而影響測(cè)試的可信度。
基于這些挑戰(zhàn),我們開(kāi)發(fā)了一套UI自動(dòng)化測(cè)試平臺(tái)- AutoMotion,旨在降低UI自動(dòng)化測(cè)試的使用門(mén)檻、提升易用性。該平臺(tái)不但能便捷地生成用例,且借助最新的大型語(yǔ)言模型,該平臺(tái)也具備了用例自愈能力,能夠智能適應(yīng)界面的合理變動(dòng),并自動(dòng)修正測(cè)試腳本;同時(shí),通過(guò)構(gòu)造數(shù)據(jù)沙箱來(lái)隔離和控制測(cè)試數(shù)據(jù),使平臺(tái)能夠確保測(cè)試的一致性和可重復(fù)性。
本文將對(duì)AutoMotion平臺(tái)的設(shè)計(jì)理念、核心功能以及實(shí)現(xiàn)原理進(jìn)行介紹,希望能與大家一同交流進(jìn)步。
背景
我們組內(nèi)曾嘗試過(guò)使用cypress等常見(jiàn)工具進(jìn)行UI自動(dòng)化測(cè)試,但遇到不少問(wèn)題和痛點(diǎn),主要?dú)w為以下四類(lèi):
手寫(xiě)腳本成本高
頁(yè)面多如繁星,手寫(xiě)自動(dòng)化測(cè)試腳本成本很高。(這點(diǎn)無(wú)需贅述)
(有些工具支持根據(jù)錄制你的操作,然后生成腳本,但普遍能力有限,比如cypress提供的這種工具就無(wú)法記錄滾動(dòng)操作)
網(wǎng)絡(luò)數(shù)據(jù)變化導(dǎo)致測(cè)試結(jié)果不可信
無(wú)論是線(xiàn)上、預(yù)發(fā)還是測(cè)試環(huán)境,數(shù)據(jù)都會(huì)不斷變化,導(dǎo)致很多情況下執(zhí)行通過(guò)與否不能正確反應(yīng)實(shí)際功能邏輯有無(wú)問(wèn)題。哪怕指定某個(gè)固定測(cè)試賬號(hào)去做某個(gè)特定用例的測(cè)試也會(huì)如此,比如存在一些非冪等的操作時(shí)便可能導(dǎo)致問(wèn)題。
舉個(gè)簡(jiǎn)單的例子。某活動(dòng)頁(yè),用戶(hù)可報(bào)名參與該活動(dòng)(每人只能報(bào)名一次),測(cè)試用例是“登錄賬號(hào)1,點(diǎn)擊活動(dòng)的報(bào)名按鈕,頁(yè)面顯示’報(bào)名成功‘” ,假如第一次執(zhí)行用例成功了,此時(shí)活動(dòng)會(huì)變?yōu)椤耙褕?bào)名”狀態(tài),第二次執(zhí)行該用例將無(wú)法再次報(bào)名,用例不通過(guò),但此時(shí)前后端代碼并無(wú)任何問(wèn)題。
當(dāng)然,可以考慮寫(xiě)個(gè)重置數(shù)據(jù)的腳本,然而:第一,需要后端同學(xué)的配合;第二,數(shù)據(jù)背后的關(guān)聯(lián)邏輯可能很復(fù)雜(比如以上例子下可以重置賬號(hào)1的報(bào)名狀態(tài),但活動(dòng)可能還會(huì)有報(bào)名結(jié)束時(shí)間,或活動(dòng)被下架,或報(bào)名名額達(dá)到上限,等等各類(lèi)情況),且針對(duì)不同項(xiàng)目不同場(chǎng)景要準(zhǔn)備不同數(shù)據(jù),實(shí)現(xiàn)成本很高;第三,很難重置線(xiàn)上數(shù)據(jù)。
項(xiàng)目迭代頻繁導(dǎo)致用例腳本維護(hù)成本高
當(dāng)頁(yè)面發(fā)生迭代后,哪怕從功能上看此次迭代與某用例無(wú)關(guān),也往往會(huì)導(dǎo)致該用例腳本失去作用,需要被更新。
參考如下例子:
測(cè)試用例是”...點(diǎn)擊任務(wù)的’去完成‘按鈕...“。
頁(yè)面結(jié)構(gòu)如圖所示:
在自動(dòng)化測(cè)試腳本中,我們可以通過(guò)“.task > .btn”這個(gè)selector選取到“去完成”按鈕,然后執(zhí)行點(diǎn)擊操作,cypress代碼為:
cy.get('.task > .btn').click()
但在后續(xù)迭代中,可能發(fā)生一些變化,比如在“.btn”外多包了一層“.task-content”:
此時(shí)通過(guò)“.task > .btn”就獲取不到了,所以我們需要在用例腳本中將selector更新為”.task > .task-content > .btn“。
當(dāng)然,在這個(gè)特定例子下,我們還可以將selector寫(xiě)成“.task .btn”,這樣即使在“.btn”外多包了任意層元素也不影響。但如果“.btn”被改成了“.btn-primary”呢?類(lèi)似的情況還有很多,很多理應(yīng)與舊用例無(wú)關(guān)的迭代,都需要我們?nèi)ジ掠美_本,大大提高了維護(hù)成本。甚至在有些時(shí)候,每次迭代都需大幅更新大量用例腳本,導(dǎo)致自動(dòng)化測(cè)試完全失去了意義,還不如人肉回歸。
如何接入標(biāo)準(zhǔn)流程中
如何把各項(xiàng)目的自動(dòng)化測(cè)試任務(wù)便捷地、靈活地接入到開(kāi)發(fā)、發(fā)布流程中。(此條也無(wú)需贅述)
為了解決以上問(wèn)題,我們決定自研一個(gè)UI自動(dòng)化測(cè)試平臺(tái):
UI自動(dòng)化測(cè)試平臺(tái) - AutoMotion
針對(duì)以上四大問(wèn)題,該平臺(tái)對(duì)應(yīng)提供了四大機(jī)制以解決:
針對(duì)以上四大問(wèn)題,該平臺(tái)對(duì)應(yīng)提供了四大機(jī)制以解決:
用例腳本錄制生成
用以解決:手寫(xiě)腳本成本高
提供chrome插件,開(kāi)啟錄制后,你可以模仿用戶(hù)在頁(yè)面中進(jìn)行一系列操作,該插件可識(shí)別你的操作,自動(dòng)生成UI自動(dòng)化測(cè)試腳本。支持點(diǎn)擊、鍵盤(pán)輸入、滾動(dòng)、拖動(dòng)等大部分常見(jiàn)操作。
數(shù)據(jù)沙箱
用以解決:網(wǎng)絡(luò)數(shù)據(jù)變化導(dǎo)致測(cè)試結(jié)果不可信
為了對(duì)之有更確切的理解,這里不妨先將目光從該問(wèn)題本身向上拉至更一般的角度。
我們可將某瀏覽器看做一個(gè)函數(shù),該函數(shù)的“輸入”為前端代碼、用戶(hù)操作、接口數(shù)據(jù)、日期、隨機(jī)數(shù)等,“輸出”為渲染后的界面,在幾乎99.9%的情況下,可以認(rèn)為該函數(shù)是確定的、無(wú)副作用的、可預(yù)測(cè)的(即在以上“輸入”保持不變的情況下,使瀏覽器運(yùn)任意次,“輸出”也應(yīng)始終不變)。
然后帶著以上視角下重新審視UI自動(dòng)化測(cè)試:
如下圖,通常,在某次迭代后執(zhí)行UI自動(dòng)化測(cè)試用例時(shí),相比前一次的執(zhí)行,“用戶(hù)操作”是不變的(寫(xiě)死在測(cè)試腳本里),但在它之外可能存在多個(gè)輸入項(xiàng)的變化,此時(shí)若渲染結(jié)果發(fā)生了變化,導(dǎo)致用例執(zhí)行失敗(中斷或不符合斷言),我們很難直接將其歸因?yàn)椤扒岸舜a變更所致”:
而若能使得每次執(zhí)行UI自動(dòng)化測(cè)試用例時(shí),僅前端代碼存在變化,若某次用例執(zhí)行失敗,便能更確切地得知其為“前端代碼變更所致”:
至此,“數(shù)據(jù)沙箱”的想法應(yīng)運(yùn)而生:
在用例錄制階段,于錄制用戶(hù)操作的同時(shí),收集過(guò)程中的接口請(qǐng)求、storage、cookies、窗口大小等外部數(shù)據(jù)(你也可以自行修改這些數(shù)據(jù)),并在用例執(zhí)行階段1:1地“復(fù)現(xiàn)”出來(lái)。
當(dāng)然,使用“數(shù)據(jù)沙箱”是有取舍的,使用后雖然避免了接口數(shù)據(jù)干擾問(wèn)題,大幅省心,但這樣相當(dāng)于只測(cè)前端代碼了。所以該平臺(tái)也支持根據(jù)需要關(guān)閉特定數(shù)據(jù)沙箱(比如關(guān)閉接口請(qǐng)求沙箱以測(cè)到后端邏輯、保留cookies沙箱以避免登錄問(wèn)題)。
用例可視化編輯、用例自愈機(jī)制
用以解決:項(xiàng)目迭代頻繁導(dǎo)致用例腳本維護(hù)成本高
首先,上文介紹的“用例腳本錄制生成”能力就已很大程度上解決了該問(wèn)題,用例需要更新時(shí)重新錄制一遍即可。
其次,該平臺(tái)還提供了:
-
“用例可視化編輯”能力,在用例僅需小幅更新時(shí),可快速編輯、更新;
-
“用例腳本編輯”能力,錄制的用例可直接轉(zhuǎn)為cypress腳本(后續(xù)考慮支持playwright),以進(jìn)行更加靈活、無(wú)約束的編輯。
這些更新方式都較為便捷,但在實(shí)際使用中依然存在一大問(wèn)題:
經(jīng)過(guò)某些迭代后執(zhí)行用例時(shí),可能很多用例執(zhí)行不通過(guò),但大部分是頁(yè)面結(jié)構(gòu)的合理更新后所至,而非新引入的bug,此時(shí)需人工一一審閱報(bào)錯(cuò)原因,確認(rèn)是feature或bug。且此次因feature導(dǎo)致的用例不通過(guò),若不及時(shí)更新,則下次執(zhí)行時(shí)將有極大概率依然不通過(guò),如此不斷疊加,不通過(guò)的用例越來(lái)越多,導(dǎo)致我們不得不在每次迭代后及時(shí)地花費(fèi)很大精力去排查、更新用例,完全違背了“自動(dòng)化測(cè)試”的初衷。
其實(shí)觀察該類(lèi)問(wèn)題后會(huì)發(fā)現(xiàn),它們大部分都出自同一個(gè)“簡(jiǎn)單”的原因:頁(yè)面更新后,用例執(zhí)行時(shí)獲取不到目標(biāo)元素(請(qǐng)回顧一下上文“③ 項(xiàng)目迭代頻繁導(dǎo)致用例腳本維護(hù)成本高”中所舉的例子)。
針對(duì)這種情況,我們對(duì)傳統(tǒng)的css selector規(guī)則進(jìn)行了擴(kuò)展,并基于現(xiàn)有的LLM實(shí)現(xiàn)了目標(biāo)元素的智能識(shí)別與“用例自愈”。使得在頁(yè)面結(jié)構(gòu)變化后(非大的功能更新),依然能夠獲取到目標(biāo)元素,并自動(dòng)根據(jù)最新頁(yè)面結(jié)構(gòu)自動(dòng)更新用例腳本。
Open API
用以解決:如何接入標(biāo)準(zhǔn)流程中
對(duì)于錄制保存后的每個(gè)用例,該平臺(tái)都會(huì)生成相應(yīng)的open api,調(diào)用該api即可執(zhí)行用例并得到結(jié)果。該api可被靈活地接入gitlab、流水線(xiàn)/構(gòu)建/發(fā)布平臺(tái)、cli工具等,實(shí)現(xiàn)在任意流程節(jié)點(diǎn)上執(zhí)行自動(dòng)化測(cè)試。
也可在該平臺(tái)上設(shè)置定時(shí)器進(jìn)行定期自動(dòng)化測(cè)試。
方案與原理介紹
以下會(huì)介紹該平臺(tái)的部分關(guān)鍵技術(shù)點(diǎn)和一些背后的思考。
用例錄入
chrome插件
用例錄入是通過(guò)chrome插件實(shí)現(xiàn)的,在開(kāi)啟錄制時(shí),該插件會(huì)進(jìn)行以下處理:
-
在頁(yè)面頭部植入一段js腳本,侵入頁(yè)面運(yùn)行時(shí)環(huán)境,通過(guò)操作頁(yè)面window對(duì)象捕獲錄制過(guò)程中的所有操作行為、storage數(shù)據(jù)等;
-
在devtools page中捕獲錄制期間所有接口數(shù)據(jù);
-
在background page(service worker)中捕獲cookies;
-
在content script中捕獲useragent、窗口大小等信息。
錄制結(jié)束后,該插件會(huì)將以上收集到的信息轉(zhuǎn)成符合我們定義規(guī)范的用例DSL,存儲(chǔ)至后臺(tái)。除了以上基礎(chǔ)能力,該插件中還實(shí)現(xiàn)了DSL可視化/編輯、元素可視化拾取、環(huán)境識(shí)別、異常檢測(cè)、用例試運(yùn)行等功能。
由于chrome插件的一些限制,在實(shí)際實(shí)現(xiàn)以上功能時(shí),需要按照chrome插件支持的能力范圍劃分模塊,且很多模塊間不能直接通信,導(dǎo)致數(shù)據(jù)流相對(duì)難以管理(所以我們對(duì)通信數(shù)據(jù)的類(lèi)型和格式進(jìn)行了一定規(guī)范,這里不展開(kāi))。部分細(xì)節(jié)如下(示意圖,僅供參考,大致瀏覽即可):
構(gòu)建與更新
chrome插件與常規(guī)前端項(xiàng)目結(jié)構(gòu)有所不同,且該chrome插件目前僅為公司內(nèi)部使用(未上傳至chrome商店),故在構(gòu)建、打包、版本更新方面也做了一些處理,封裝在我們的構(gòu)建腳本中,運(yùn)行后可自動(dòng)完成如下流程:
-
使用webpack對(duì)devtools頁(yè)面(即本插件中所有UI展示的部分)進(jìn)行構(gòu)建;
-
對(duì)devtools頁(yè)面以外全部文件進(jìn)行構(gòu)建(如統(tǒng)一替換環(huán)境變量);
-
更新manifest中版本號(hào);
-
將整個(gè)項(xiàng)目壓縮為zip包,上傳至線(xiàn)上;
-
更新數(shù)據(jù)庫(kù)中最新版本號(hào);
-
(用戶(hù)在打開(kāi)舊版本chrome插件時(shí)會(huì)提示更新,點(diǎn)擊即可下載)。
用例執(zhí)行
我們是通過(guò)nodejs和cypress實(shí)現(xiàn)用例執(zhí)行的。nodejs服務(wù)會(huì)在容器中初始化cypress項(xiàng)目環(huán)境,在需執(zhí)行用例時(shí),將用例DSL轉(zhuǎn)為可執(zhí)行的cypress腳本和相關(guān)數(shù)據(jù)文件,其中包含用戶(hù)操作、數(shù)據(jù)沙箱等信息,然后通過(guò)cypress運(yùn)行(支持?jǐn)?shù)個(gè)cypress實(shí)例并發(fā)),最后生成運(yùn)行結(jié)果,并觸達(dá)給前臺(tái)觸發(fā)方或預(yù)警方。
行為監(jiān)聽(tīng)
在上文中已經(jīng)提到了,在用例錄制時(shí),chrome插件會(huì)在頁(yè)面頭部植入一段js腳本,監(jiān)聽(tīng)操作者行為,這里進(jìn)一步闡述一下監(jiān)聽(tīng)方式。
基礎(chǔ)事件監(jiān)聽(tīng)
chrome插件植入的js腳本會(huì)在頁(yè)面window對(duì)象上進(jìn)行全局事件委托,以監(jiān)聽(tīng)用戶(hù)相關(guān)操作。如:
window.addEventListener('input', this.handleInput, true)
特殊事件監(jiān)聽(tīng)
以上“基礎(chǔ)事件監(jiān)聽(tīng)”方式似乎已經(jīng)足夠,但細(xì)想一下,mouseenter、mouseleave、mousemove、mouseover、mouseout等事件也能通過(guò)這種方式綁在window或document節(jié)點(diǎn)上進(jìn)行監(jiān)聽(tīng)嗎?
顯然不合理,比如若在document上監(jiān)聽(tīng)mouseenter事件,那么當(dāng)我們的鼠標(biāo)劃過(guò)頁(yè)面中任意元素時(shí),都會(huì)觸發(fā)該事件,這些信息是冗余的。可以對(duì)比一下click,因?yàn)橛脩?hù)在頁(yè)面中的大部分點(diǎn)擊操作都是“有意義”的(是整個(gè)”頁(yè)面函數(shù)“中的一個(gè)有意義的輸入),所以整個(gè)頁(yè)面中任意位置觸發(fā)click是都可以記錄,而大部分鼠標(biāo)移動(dòng)操作都對(duì)頁(yè)面無(wú)實(shí)際意義。
這里應(yīng)當(dāng)反過(guò)來(lái)思考:只有本身已經(jīng)綁定了這些事件的元素,觸發(fā)這些事件對(duì)它來(lái)說(shuō)才大概率是有意義的,我們對(duì)這些元素做監(jiān)聽(tīng)就行。比如頁(yè)面中有個(gè)元素A,它被綁定了mouseenter事件以在鼠標(biāo)移入時(shí)展示tips,那么在用例錄制時(shí),若用戶(hù)將鼠標(biāo)移入元素A,觸發(fā)了tips展示,就需要將該mouseenter事件錄制下來(lái)、在用例執(zhí)行時(shí)觸發(fā),否則就無(wú)需記錄。那么如何知道哪些元素被綁定了這些事件呢?可以將頁(yè)面中window.Element.prototype.addEventListener函數(shù)替換成我們自己構(gòu)造的函數(shù),實(shí)現(xiàn)對(duì)任意頁(yè)面中原本的所有”事件綁定“行為的攔截,當(dāng)然同時(shí)也需調(diào)用原函數(shù),不影響原功能。代碼示意如下:
const originalAddEventListener = window.Element.prototype.addEventListener;
// 替換所有元素的addEventListener
window.Element.prototype.addEventListener = function(type, originalListener, ...others) {// 替換所有元素的listener回調(diào)函數(shù),進(jìn)而攔截mouseenter、mouseleave、mouseover、mouseout事件const listener = function(event) {if (isRecording && ['mouseenter', 'mouseleave', 'mouseover', 'mouseout', 'mousemove'].includes(event.type) && event.target === this) {handleMouseAction(event)}originalListener.call(this, event)}return originalAddEventListener.call(this, type, listener, ...others)
}
目標(biāo)元素定位
“行為”實(shí)際上包括“動(dòng)作”和“作用對(duì)象”(即“目標(biāo)元素”)兩塊。比如“用戶(hù)點(diǎn)擊元素A”,其中“動(dòng)作”是“點(diǎn)擊”,“目標(biāo)元素”是“元素A”。上文已經(jīng)介紹了如何識(shí)別“動(dòng)作”,這里介紹如何定位“目標(biāo)元素”。
基礎(chǔ)定位
以“從根節(jié)點(diǎn)到目標(biāo)元素的唯一路徑的css selector”作為定位方式。如:body > div > #app > div.app-right > div > div:nth-child(2) > div.text
基礎(chǔ)定位增強(qiáng)
請(qǐng)先看一個(gè)例子,如下圖,“導(dǎo)出”按鈕是目標(biāo)元素,通過(guò)“基礎(chǔ)定位”記錄下的selector可能是“xxx > div:nth-child(2)”:
但在某次迭代后,在“導(dǎo)出”之前新增了一個(gè)“重置”按鈕:
此時(shí)“xxx > div:nth-child(2)”定位到的就是“重置”按鈕了。
這里我們會(huì)發(fā)現(xiàn),常規(guī)css selector的描述能力似乎并不夠,對(duì)于某個(gè)元素,你可以準(zhǔn)確描述它的class,也可以準(zhǔn)確描述它是當(dāng)前層級(jí)中的第幾個(gè)元素,但無(wú)法將二者結(jié)合,即無(wú)法描述“它是當(dāng)前層級(jí)中class為xx的第幾個(gè)元素”(前端界的“測(cè)不準(zhǔn)原理”?😑)。
所以不如定義一套新的規(guī)則(基于css selector進(jìn)行擴(kuò)展),如“xx > div.btn.primary:nth(2)”,含義為“在xx的子節(jié)點(diǎn)中,選取所有符合div.btn.primary的節(jié)點(diǎn),再在其中選取第2個(gè)”,這樣放在上面的例子里,新增“重置”按鈕后也不會(huì)影響到“導(dǎo)出”按鈕的定位了。
此外,我們還可以加入文本內(nèi)容信息,如“xx > div.btn.primary:contains('導(dǎo)出'):nth(1)”,這樣哪怕新增的“重置”按鈕也是“.btn.primary”,也不影響了(當(dāng)然,若信息過(guò)多,在頁(yè)面迭代后、進(jìn)行精準(zhǔn)匹配時(shí)反而更容易出錯(cuò)。但倘若是“模糊匹配”,則信息越多越好,這點(diǎn)下面會(huì)探討)。
(其實(shí)xpath就能夠描述這些信息,但最新的cypress已不支持xpath了,而手動(dòng)實(shí)現(xiàn)xpath查找的成本很高,所以我們選擇手動(dòng)實(shí)現(xiàn)自定義增強(qiáng)版的“css selector”)
基于LLM的目標(biāo)元素智能識(shí)別與用例自愈
方案
使用以上增強(qiáng)版的css selector在部分情況下能夠降低頁(yè)面結(jié)構(gòu)變化后元素定位失敗的概率,但依然有很多情況會(huì)導(dǎo)致失敗,比如元素class改變、元素位置改變、元素內(nèi)容文本改變等。
可以想象一下,在人工回歸時(shí),大部分情況下我們“聰明的人類(lèi)”能輕松地識(shí)別出變化后的目標(biāo)元素,那么能否讓UI自動(dòng)化測(cè)試擁有接近人類(lèi)的目標(biāo)元素識(shí)別能力呢?
一個(gè)最顯然的方案是:基于圖像識(shí)別選取元素。這原本也幾乎是唯一最佳選擇,但隨著近幾年LLM的迅速發(fā)展,我們想到了另一種可能:基于LLM實(shí)現(xiàn)目標(biāo)元素智能識(shí)別。畢竟最新LLM模型的文本理解能力已經(jīng)相當(dāng)驚人,而“頁(yè)面結(jié)構(gòu)”本身也是通過(guò)文本(dom)進(jìn)行描述的。因?yàn)閷?duì)圖像識(shí)別相關(guān)經(jīng)驗(yàn)不多,且第二種方案也有望擴(kuò)展至“目標(biāo)元素智能識(shí)別”以外的各種場(chǎng)景,故選取了第二種。
該方案大致為:在用例執(zhí)行中,若selector獲取失敗,則暫停執(zhí)行,將頁(yè)面dom、selector等信息交由LLM進(jìn)行處理,讓其識(shí)別出該“過(guò)時(shí)的”selector真正想獲取的元素是什么,返回更新后的selector路徑,然后更新相應(yīng)用例并重新執(zhí)行,達(dá)到“用例自愈”。
DOM壓縮
在將信息輸入LLM前,有一個(gè)很明顯的問(wèn)題:頁(yè)面dom可能會(huì)很大,導(dǎo)致LLM處理時(shí)間很長(zhǎng),甚至可能直接超過(guò)LLM支持的大小上限。比如這是b站首頁(yè)渲染完成后的dom大小:
高達(dá)22萬(wàn)tokens!不過(guò)其中大部分信息對(duì)“目標(biāo)元素識(shí)別”來(lái)說(shuō)都幾乎是無(wú)意義的,我們可以將它們?nèi)縿h除,進(jìn)行“dom壓縮”:
-
刪除script、style、link等結(jié)構(gòu)無(wú)關(guān)tag
-
空格、換行合并
-
對(duì)于剩下的所有tag,僅保留tag、id、class、text信息,其它全部刪除
經(jīng)過(guò)這些步驟后,該頁(yè)面占用的tokens大幅降低至1.3萬(wàn):
可剩下的dom中重復(fù)內(nèi)容還是不少的。比如某個(gè)class名可能在頁(yè)面中出現(xiàn)很多次,占用了很多字符。所以可生成一個(gè)”class壓縮映射表“,把每個(gè)class名映射為一個(gè)數(shù)字,最終可能用0-500這些數(shù)字就能表示所有class名,當(dāng)然,數(shù)字每位”密度“還是太低了(只能包含10個(gè)不同的值),且class名開(kāi)頭不能為數(shù)字,所以用字母”計(jì)數(shù)“更佳,比如先從a到z,再+1就是aa,以此類(lèi)推,大部分class名都會(huì)被壓縮至兩個(gè)字符以?xún)?nèi)(經(jīng)過(guò)試驗(yàn),僅此步就可將該頁(yè)面tokens數(shù)進(jìn)一步降低至0.9萬(wàn))。
同理,tag名稱(chēng)也可壓縮。甚至”class“這個(gè)字段本身也可壓縮(比如壓縮成”c“)。
經(jīng)過(guò)這些壓縮后,dom確實(shí)會(huì)繼續(xù)縮小,然而這些壓縮會(huì)丟失不少”有語(yǔ)義“的信息,無(wú)論是一些class名(比如”task-btn“)還是tag名,它們有助于LLM去理解頁(yè)面內(nèi)容、做出更準(zhǔn)確的目標(biāo)元素識(shí)別,所以我們最終未選擇這些進(jìn)一步壓縮的方式(另一種方式是將壓縮映射表也告知LLM,不過(guò)也會(huì)提高LLM理解難度,有待試驗(yàn))。
實(shí)際上,b站首頁(yè)算是b站dom最龐大的頁(yè)面之一了,大部分頁(yè)面經(jīng)過(guò)第一步的壓縮后,tokens會(huì)遠(yuǎn)低于1.3萬(wàn)(比如很多移動(dòng)端頁(yè)面壓縮后tokens數(shù)為1500-5000)。
Prompt
壓縮完dom后,便可以構(gòu)造promot了,我們的prompt結(jié)構(gòu)大致如下:
其中會(huì)先通過(guò)一些例子讓LLM學(xué)會(huì)如何進(jìn)行“模糊匹配”(即根據(jù)“不準(zhǔn)確的selector”找到正確的元素),然后將壓縮后的dom、selector交由它完成任務(wù)。
對(duì)于實(shí)際效果,我們進(jìn)行了一些初步試驗(yàn):
在一般情況下,“用例自愈”能力已可滿(mǎn)足要求(仍在優(yōu)化中)。對(duì)于確實(shí)變化很大難以“自愈”的,將提示用戶(hù)重新錄制用例。
執(zhí)行流程
采用標(biāo)元素智能識(shí)別與用例自愈后的執(zhí)行流程如下:
Fine-Tuning?
目前我們對(duì)LLM的所有要求都被囊括在prompt中,這可能并不是最佳方式,畢竟prompt的長(zhǎng)度有上限,我們無(wú)法灌輸給LLM足夠多的信息去提高正確率,也會(huì)消耗較多處理時(shí)間和費(fèi)用。
而fine-tuning允許我們根據(jù)自己的特定需求和數(shù)據(jù)集來(lái)定制和優(yōu)化模型,以適應(yīng)特定場(chǎng)景,相比上面的純prompt方案,我們可以預(yù)先喂給大模型多得多的“模糊匹配”例子,進(jìn)一步提高其識(shí)別目標(biāo)元素的能力,且prompt大小可大幅縮減(prompt中“前置能力學(xué)習(xí)”的部分可以去掉了)。所以fine-tuning應(yīng)該是更好的方式,由于當(dāng)下時(shí)間有限暫未嘗試,后續(xù)會(huì)深入。
唯一標(biāo)識(shí)符定位
基礎(chǔ)方案
上面介紹了常規(guī)定位+智能定位的方案,已經(jīng)可以相對(duì)絲滑地覆蓋大部分場(chǎng)景,但有時(shí)LLM也會(huì)出錯(cuò)以及部分敏感頁(yè)面可能不適合傳入LLM。有沒(méi)有其它可選的、接入成本稍高一點(diǎn)的、但更加穩(wěn)定的方式呢?當(dāng)然有,很直接:為所有目標(biāo)元素加上唯一標(biāo)識(shí)。
可于用例錄制前,為需要錄制操作的元素綁上唯一標(biāo)識(shí),如:“
<div data-atm-id="(uuid)" ”(這里沒(méi)有直接使用“id”屬性是為了避免影響原項(xiàng)目業(yè)務(wù)邏輯),在用例錄制時(shí),若目標(biāo)元素有此類(lèi)標(biāo)識(shí),則記錄該標(biāo)識(shí)即可。因?yàn)樵摌?biāo)識(shí)全局唯一,故任何dom結(jié)構(gòu)的變動(dòng)后都不影響獲取該元素(除非刪除該元素,當(dāng)然,這時(shí)也說(shuō)明該用例已與新需求不符,應(yīng)重新錄制用例了)。
問(wèn)題與改進(jìn)
有一些細(xì)節(jié)但關(guān)鍵的問(wèn)題需要處理:
問(wèn)題1:
若只有元素2綁定了唯一標(biāo)識(shí),而錄制時(shí)是元素1被點(diǎn)擊,對(duì)不上,這樣記錄下的依然是元素1的selector。
解法:用例錄制時(shí),若被捕獲的元素沒(méi)有data-atm-id,則向上找到離其最近的含data-atm-id的節(jié)點(diǎn),作為真正的目標(biāo)元素并記錄。
問(wèn)題2:難以給一些npm組件庫(kù)中某組件的內(nèi)部元素打上標(biāo)識(shí)。
解法:增加“data-atm-parentId”標(biāo)識(shí),寫(xiě)在組件最外層(若不方便寫(xiě),也可在組件外自己加一層div寫(xiě)上該標(biāo)識(shí)),用例錄制時(shí),若被捕獲的元素沒(méi)有data-atm-id,(且在問(wèn)題1解法未滿(mǎn)足時(shí)),向上找到離其最近的含data-atm-parentId的節(jié)點(diǎn),構(gòu)建以它為開(kāi)頭的selector(如“[data-atm-parentId="xxx"] > div”),若找不到才記錄當(dāng)前元素的常規(guī)selector。
權(quán)衡
當(dāng)然,唯一標(biāo)識(shí)符定位也存在一些弊端:
-
有一定接入成本,需侵入被測(cè)項(xiàng)目,修改其項(xiàng)目代碼;
-
處理組件庫(kù)內(nèi)部元素的方式依然不夠穩(wěn)定可靠;
-
倘若后續(xù)想進(jìn)一步實(shí)現(xiàn)“全自動(dòng)”用例生成(結(jié)尾“展望”部分會(huì)介紹)——要對(duì)成百上千個(gè)頁(yè)面生成用例,此方案就不適用了。
可見(jiàn),在”元素定位“方面,幾乎沒(méi)有既簡(jiǎn)單高效、又穩(wěn)定可靠的完美方案,現(xiàn)在可以做的是將以上方案結(jié)合,讓使用者能夠按需要自行選擇。不過(guò),我們認(rèn)為在“目標(biāo)元素智能識(shí)別與用例自愈“上仍然有不斷優(yōu)化的空間,也是我們主要著眼的方向,且在使用fine-tuning或更強(qiáng)大的LLM問(wèn)世后,此方案大概率將得到又一次增強(qiáng)。
數(shù)據(jù)沙箱
這里介紹一下“數(shù)據(jù)沙箱”中的核心部分:“網(wǎng)絡(luò)請(qǐng)求沙箱”。
基礎(chǔ)方案
基礎(chǔ)方案很簡(jiǎn)單:
-
用例錄制時(shí),通過(guò)chrome插件的chrome.devtools方法捕獲所有接口數(shù)據(jù);
-
用例執(zhí)行時(shí),通過(guò)接口入?yún)?#xff08;url(含query)+method+body)匹配錄制時(shí)記錄下的相應(yīng)網(wǎng)絡(luò)數(shù)據(jù)、通過(guò)cypress的cy.intercept方法進(jìn)行攔截和mock。
問(wèn)題與改進(jìn)方案
基礎(chǔ)方案乍一看挺美好,但仔細(xì)一想坑不少。
問(wèn)題1:“url(含query)+method+body”并不足以精準(zhǔn)匹配。比如同樣的接口被先后請(qǐng)求兩次,但返回值不一樣,基礎(chǔ)方案下記錄的信息并不能區(qū)分這兩次接口請(qǐng)求。舉例:用戶(hù)進(jìn)入某活動(dòng)頁(yè),調(diào)用接口a獲取報(bào)名狀態(tài),返回“未報(bào)名”,點(diǎn)擊報(bào)名按鈕,成功,重新調(diào)用接口a,返回“已報(bào)名”,用例執(zhí)行時(shí)如何得知每次接口a的調(diào)用應(yīng)該返回什么呢?
解法:兩次接口調(diào)用之間,接口入?yún)⑼耆蛔?#xff0c;那什么發(fā)生了變化可能導(dǎo)致接口出參改變呢?操作行為和時(shí)間。所以在記錄每個(gè)接口信息時(shí),我們還會(huì)進(jìn)行“數(shù)據(jù)歸因”+“時(shí)間對(duì)齊”,將其“歸因”到某個(gè)用例節(jié)點(diǎn)上,并記錄相對(duì)時(shí)間。如:接口a歸因于“點(diǎn)擊報(bào)名按鈕”節(jié)點(diǎn),滯后200ms左右。
問(wèn)題2:有些項(xiàng)目的接口入?yún)⒅袝?huì)包含一些前端生成的隨機(jī)信息,導(dǎo)致執(zhí)行用例時(shí)每次的入?yún)⒍疾灰粯?#xff0c;匹配不上(有了“隨機(jī)數(shù)沙箱”后倒可以解決,還未實(shí)現(xiàn));頁(yè)面迭代可能會(huì)修改接口入?yún)?#xff0c;導(dǎo)致匹配不上;頁(yè)面迭代后運(yùn)行時(shí)間可能變化較大,導(dǎo)致歸因的時(shí)間對(duì)不上;等等。
解法:這些問(wèn)題都是“精準(zhǔn)匹配”導(dǎo)致的,其實(shí)解決思路與上文的目標(biāo)元素定位類(lèi)似,即增加“模糊匹配”。這里目前不需要LLM,因?yàn)槠ヅ湫畔⒔Y(jié)構(gòu)固定且可被量化,所以我們制定了"接口相似度匹配算法",根據(jù)url、query相似度(kv匹配、k匹配、多余的k等)、method、距歸因節(jié)點(diǎn)時(shí)間等指標(biāo)綜合計(jì)算得分,選擇得分最高的接口然后返回。(若無(wú)一個(gè)得分高的,則發(fā)往真實(shí)后端)
問(wèn)題3:頁(yè)面迭代后可能會(huì)引入新接口,該接口甚至可能跟用例所測(cè)模塊毫無(wú)關(guān)系(所以正常來(lái)說(shuō),肯定不想因?yàn)樗ジ掠美?#xff09;,但因匹配不上,會(huì)發(fā)往后端,此時(shí)若無(wú)登錄態(tài)會(huì)跳登錄頁(yè)或使頁(yè)面出錯(cuò),導(dǎo)致用例失敗。
解法:用例錄制時(shí)記錄下cookies,用例執(zhí)行時(shí)注入。這樣通過(guò)”模糊匹配“也匹配不上的接口,會(huì)攜cookies發(fā)往后端,至少大概率保證頁(yè)面不報(bào)錯(cuò)。
如此,我們將“接口數(shù)據(jù)沙箱”劃分為如下圖所示的三層,每當(dāng)上一層匹配失敗時(shí),觸發(fā)下一層:
多環(huán)境與多用例并發(fā)執(zhí)行
如圖,由于需要支持在不同環(huán)境執(zhí)行用例,所以我們將“用例執(zhí)行”的部分單獨(dú)拆了出來(lái),作為“runner”,分別部署在不同環(huán)境,”runner“容器中也需預(yù)先裝好無(wú)頭瀏覽器、cypress和相關(guān)運(yùn)行環(huán)境;將常規(guī)的crud部分作為”server“,同時(shí)它也負(fù)責(zé)將用例分發(fā)至對(duì)應(yīng)環(huán)境的”runner“。
此外,每個(gè)用例的執(zhí)行需占用一定資源和時(shí)間,執(zhí)行器支持的最大用例并發(fā)數(shù)有上限,所以,用例進(jìn)入runner時(shí)會(huì)進(jìn)入隊(duì)列中等候,在執(zhí)行器有空余時(shí)執(zhí)行。
主要流程如下:
-
在用例錄制時(shí),獲取每個(gè)url最終指向的ip地址,自動(dòng)識(shí)別出當(dāng)前運(yùn)行環(huán)境,保存在用例中(也支持手動(dòng)修改環(huán)境);
-
觸發(fā)用例執(zhí)行時(shí),server將用例發(fā)往對(duì)應(yīng)環(huán)境的runner執(zhí)行;
-
用例進(jìn)入runner的隊(duì)列中等待執(zhí)行。
DSL
我們測(cè)試框架代碼之上定義了DSL層,它是對(duì)于用例的結(jié)構(gòu)化描述,JSON格式,包含某個(gè)用例的全部信息。它的作用是:
-
便于用例錄制時(shí)快速生成,便于存儲(chǔ);
-
便于可視化展示以及可視化編輯;
-
對(duì)用例進(jìn)行結(jié)構(gòu)上的約束,減小出錯(cuò)概率,收斂系統(tǒng)復(fù)雜度;
-
理論上可被轉(zhuǎn)為各類(lèi)測(cè)試框架代碼并執(zhí)行。
導(dǎo)出用例腳本并編輯
部分用例需高度定制,通過(guò)可視化編輯無(wú)法滿(mǎn)足,因而我們提供了將用例DSL導(dǎo)出為cypress腳本的功能(后續(xù)考慮支持playwright腳本),通過(guò)修改cypress腳本便能實(shí)現(xiàn)一切cypress可實(shí)現(xiàn)的能力。
從上方“DSL“章節(jié)的圖中可知,DSL可以轉(zhuǎn)成cypress腳本代碼,但該過(guò)程不可逆(DSL的能力是cypress腳本的子集),所以在用戶(hù)導(dǎo)出cypress腳本后,該腳本會(huì)成為新的用例,作為”腳本類(lèi)型用例“與”DSL類(lèi)型用例“區(qū)分開(kāi)來(lái)。
我們提供了cli工具,可在任意目錄下初始化cypress項(xiàng)目,然后可將任意用例轉(zhuǎn)為cypress腳本下載至此,以便在本地修改和運(yùn)行調(diào)試,修改完成后可通過(guò)cli將用例上傳至線(xiàn)上,成為一個(gè)新的”腳本類(lèi)型用例“?!蹦_本類(lèi)型用例“能夠與”DSL類(lèi)型用例“一樣在線(xiàn)上執(zhí)行。
流程示意如下:
展望
現(xiàn)在已經(jīng)實(shí)現(xiàn)了錄制后自動(dòng)生成用例腳本、代碼小幅更新后自動(dòng)修復(fù)用例腳本,但“錄制”操作本身依然需要人工執(zhí)行,而且在這之前還需制定用例,能否把這些過(guò)程也“自動(dòng)”做掉?
一個(gè)直接的想法是:讓LLM了解產(chǎn)品背景、閱讀最新產(chǎn)品需求,生成用例。理論上讓LLM生成文字描述的用例顯然是可行的,而想要進(jìn)一步生成用例DSL或用例腳本,還需使LLM知曉所有相關(guān)頁(yè)面運(yùn)行時(shí)的具體結(jié)構(gòu),如何運(yùn)行、如何存儲(chǔ)、如何理解、如何關(guān)聯(lián)等都會(huì)是難題。
所以不如換一個(gè)角度:頁(yè)面最終是服務(wù)于線(xiàn)上用戶(hù)的,他們的操作本身不就是最好的”用例“么?所以可以自動(dòng)采集線(xiàn)上線(xiàn)上用戶(hù)的真實(shí)操作鏈路,自動(dòng)識(shí)別出高頻鏈路、核心鏈路,自動(dòng)歸總為用例。相比之下,該方案更易實(shí)現(xiàn)(當(dāng)然尚待深入的細(xì)節(jié)問(wèn)題依然不少),且生成的用例質(zhì)量甚至可能更好,同時(shí)也有借助LLM繼續(xù)優(yōu)化的可能性。
流程示意如下:
如此,在設(shè)定好策略后,便可自動(dòng)批量產(chǎn)出用例、自動(dòng)執(zhí)行,做到“全流程全自動(dòng)”,這才是真正“自動(dòng)”的“自動(dòng)化測(cè)試”。預(yù)期主要可以服務(wù)于數(shù)量龐大的、非核心的前端頁(yè)面(數(shù)量太多難以一個(gè)個(gè)錄入和維護(hù),但又想一定程度上保證質(zhì)量),大幅降低生產(chǎn)和維護(hù)用例的時(shí)間。