測(cè)試金字塔理論被廣泛應(yīng)用于計(jì)劃和實(shí)施敏捷軟件開發(fā)所倡導(dǎo)的測(cè)試自動(dòng)化,并且取得了令人矚目的成就。本文嘗試從產(chǎn)品開發(fā)的角度出發(fā),結(jié)合Kent Beck最近提出的3X模型和近年來迅速發(fā)展的自動(dòng)化測(cè)試技術(shù),提出并討論一種新的測(cè)試層級(jí)動(dòng)態(tài)平衡觀:三明治模型。同時(shí),為了應(yīng)對(duì)端到端測(cè)試在實(shí)踐中面臨的種種挑戰(zhàn),設(shè)計(jì)并實(shí)現(xiàn)了一種面向用戶旅程的端到端自動(dòng)化測(cè)試框架——雪鸮。實(shí)際項(xiàng)目經(jīng)驗(yàn)表明,雪鸮能夠顯著提升端到端測(cè)試的可維護(hù)性,減少不確定性影響,幫助開發(fā)人員更快定位和修復(fù)問題,對(duì)特定時(shí)期的產(chǎn)品開發(fā)活動(dòng)更具吸引力。
背景
測(cè)試金字塔
按照自動(dòng)化測(cè)試的層級(jí),從下至上依次為單元測(cè)試、集成測(cè)試和端到端測(cè)試,盡量保持?jǐn)?shù)量較多的低層單元測(cè)試,以及相對(duì)較少的高層端到端測(cè)試,這就是測(cè)試金字塔理論。隨著敏捷軟件開發(fā)的日益普及,測(cè)試金字塔逐漸為人所知,進(jìn)而得到廣泛應(yīng)用。Mike Cohn、Martin Fowler以及Mike Wacker等先后對(duì)測(cè)試金字塔進(jìn)行了很好的詮釋和發(fā)展,其主要觀點(diǎn)如下:
- 測(cè)試層級(jí)越高,運(yùn)行效率就越低,進(jìn)而延緩持續(xù)交付的構(gòu)建-反饋循環(huán)。
- 測(cè)試層級(jí)越高,開發(fā)復(fù)雜度就越高,如果團(tuán)隊(duì)能力受限,交付進(jìn)度就會(huì)受到影響。
- 端到端測(cè)試更容易遇到測(cè)試結(jié)果的不確定性問題,按照Martin Fowler的說法,這種結(jié)果不確定性的測(cè)試毫無意義。
- 測(cè)試層級(jí)越低,測(cè)試的代碼隔離性越強(qiáng),越能幫助開發(fā)人員快速定位和修復(fù)問題。
3X模型
2016年起,敏捷和TDD先驅(qū)Kent Beck開始在個(gè)人Facebook主頁撰寫系列文章,闡述產(chǎn)品開發(fā)的三個(gè)階段——Explore、Expand和Extract,以及在不同階段中產(chǎn)品與工程實(shí)踐之間的關(guān)系問題,即3X模型。近二十年軟硬件技術(shù)的飛速發(fā)展,使得軟件開發(fā)活動(dòng)面臨敏捷早期從未遇到的市場(chǎng)變革,而根據(jù)在Facebook工作的經(jīng)歷,Kent Beck把產(chǎn)品開發(fā)總結(jié)為三個(gè)階段:
- 探索(Explore),此時(shí)的產(chǎn)品開發(fā)仍處于非常初期的階段,仍然需要花費(fèi)大量時(shí)間尋找產(chǎn)品和市場(chǎng)的適配點(diǎn),也是收益最低的階段。
- 擴(kuò)張(Expand),一旦產(chǎn)品擁有助推器(通常意味著已經(jīng)找到了市場(chǎng)的適配點(diǎn)),市場(chǎng)需求就會(huì)呈現(xiàn)指數(shù)級(jí)上升,產(chǎn)品本身也需要具備足夠的伸縮性以滿足這些需求,由此收益也會(huì)快速上升。
- 提?。‥xtract),當(dāng)位于該階段時(shí),公司通常希望最大化產(chǎn)品收益。但此時(shí)收益的增幅會(huì)小于擴(kuò)張階段。

(3X)
Kent Beck認(rèn)為,如果以產(chǎn)品是否成功作為衡量依據(jù),那么引入自動(dòng)化測(cè)試在探索階段的作用就不大,甚至?xí)泳彯a(chǎn)品接受市場(chǎng)反饋循環(huán)的速度,對(duì)產(chǎn)品的最終成功毫無用處,還不如不引入;當(dāng)位于擴(kuò)張階段時(shí),市場(chǎng)一方面要求產(chǎn)品更高的伸縮性,另一方面也開始要求產(chǎn)品保證一致的行為(例如質(zhì)量需求),那么此時(shí)就該引入自動(dòng)化測(cè)試來保證產(chǎn)品的行為一致性;當(dāng)產(chǎn)品最終處于提取階段時(shí),任何改動(dòng)都應(yīng)以不犧牲現(xiàn)有行為為前提,否則由此引發(fā)的損失可能遠(yuǎn)高于改動(dòng)帶來的收益,此時(shí)自動(dòng)化測(cè)試就扮演了非常重要的角色。
測(cè)試工具爆炸式增長和綜合技能學(xué)習(xí)曲線陡升
根據(jù)SoftwareQATest網(wǎng)站的歷史數(shù)據(jù),2010年記錄的測(cè)試工具有440個(gè),共劃分為12個(gè)大類。這個(gè)數(shù)字到2017年已經(jīng)變?yōu)?60個(gè),共15個(gè)大類,且其中有340個(gè)在2010年之后才出現(xiàn)。也就是說,平均每年就有50個(gè)新的測(cè)試工具誕生。
面對(duì)測(cè)試工具的爆炸式增長,一方面所支持的測(cè)試類型更加完善,更加有利于在產(chǎn)品開發(fā)過程中保證產(chǎn)品的一致性;另一方面也導(dǎo)致針對(duì)多種測(cè)試工具組合的綜合技能學(xué)習(xí)曲線不斷上升。在實(shí)踐中,團(tuán)隊(duì)也往往對(duì)如何定義相關(guān)測(cè)試的覆蓋范圍感到不知所措,難以真正發(fā)揮測(cè)試工具的效用,也很難對(duì)產(chǎn)品最終成功作出應(yīng)有的貢獻(xiàn)。
從金字塔到三明治
作為敏捷在特定時(shí)期的產(chǎn)物,測(cè)試金字塔并不失其合理性,甚至還對(duì)自動(dòng)化測(cè)試起到了重要推廣作用。但是,隨著行業(yè)整體技術(shù)能力的不斷提升,市場(chǎng)需求和競(jìng)爭(zhēng)日趨激烈,在項(xiàng)目中具體實(shí)施測(cè)試金字塔時(shí)往往遭遇困難,即便借助外力強(qiáng)推,其質(zhì)量和效果也難以度量。
此外,隨著軟件設(shè)計(jì)和開發(fā)技術(shù)的不斷發(fā)展,低層單元測(cè)試的傳統(tǒng)測(cè)試技術(shù)和落地,因前、后端技術(shù)棧的多樣化而大相徑庭;同時(shí),在經(jīng)歷過覆蓋率之爭(zhēng),如何確保單元測(cè)試的規(guī)范和有效,也成為工程質(zhì)量管理的一大挑戰(zhàn);高層的端到端測(cè)試則基本不受技術(shù)棧頻繁更替的影響,隨著不同載體上driver類技術(shù)的不斷成熟,其開發(fā)復(fù)雜度反而逐漸降低。
這里討論一種新的測(cè)試層級(jí)分配策略,我們稱之為三明治模型 。如下圖所示,該模型允許對(duì)不同測(cè)試層級(jí)的占比進(jìn)行動(dòng)態(tài)調(diào)整,說明了倒金字塔形、沙漏形以及金字塔形分配對(duì)特定產(chǎn)品開發(fā)階段的積極作用。

(Sandwich)
產(chǎn)品開發(fā)的自動(dòng)化測(cè)試策略
根據(jù)3X模型,在探索初期往往選擇避開自動(dòng)化測(cè)試。一旦進(jìn)入擴(kuò)張期,產(chǎn)品的可伸縮性和行為一致性就成為共同目標(biāo),但此時(shí)也常會(huì)發(fā)生大的代碼重構(gòu)甚至重寫,如果沿用測(cè)試金字塔,無論補(bǔ)充缺失的單元測(cè)試,還是只對(duì)新模塊寫單元測(cè)試,都既損害了產(chǎn)品的快速伸縮能力,也無法保證面向用戶的產(chǎn)品行為一致性。因此,如果在探索后期先引入高層的端到端測(cè)試,覆蓋主要用戶旅程,那么擴(kuò)張期內(nèi)所產(chǎn)生的一系列改動(dòng)都能夠受到端到端測(cè)試的保障。
需要注意的是,用戶旅程在產(chǎn)品即將結(jié)束探索期時(shí)通常會(huì)趨于穩(wěn)定,在擴(kuò)張期出現(xiàn)顛覆性變化的概率會(huì)逐漸減少,端到端測(cè)試的增長率會(huì)逐步下降。
除此以外,隨著擴(kuò)張期內(nèi)不斷產(chǎn)生的模塊重構(gòu)和服務(wù)化,團(tuán)隊(duì)還應(yīng)增加單元測(cè)試和集成測(cè)試的占比。其中,單元測(cè)試應(yīng)確保覆蓋分支場(chǎng)景(可以在CI中引入基于模塊的覆蓋率檢測(cè));集成測(cè)試和某些團(tuán)隊(duì)實(shí)踐的驗(yàn)收測(cè)試,則需進(jìn)一步覆蓋集成條件和驗(yàn)收條件(在story sign-off和code review時(shí)驗(yàn)收)。
許多新興的測(cè)試技術(shù)和工具擅長各自場(chǎng)景下的驗(yàn)收測(cè)試,但更重要的仍是識(shí)別產(chǎn)品階段和當(dāng)前需求,以滿足收益最大化。

(Sandwich-3x)
由此我們認(rèn)為,隨著產(chǎn)品開發(fā)的演進(jìn),測(cè)試層級(jí)的分配應(yīng)參考三明治模型,動(dòng)態(tài)調(diào)整層級(jí)占比,更加重視運(yùn)營和市場(chǎng)反饋,致力于真正幫助產(chǎn)品走向成功。
端到端測(cè)試的機(jī)遇和挑戰(zhàn)
與其他測(cè)試層級(jí)相比,端到端測(cè)試技術(shù)的發(fā)展程度相對(duì)滯后。一方面,作為其基礎(chǔ)的driver工具要在相應(yīng)載體成熟一段時(shí)間之后才能趨于穩(wěn)定,web、mobile無不如是。另一方面,端到端測(cè)試偏向黑盒測(cè)試,更加側(cè)重描述用戶交互和業(yè)務(wù)功能,尋求硬核技術(shù)突破的難度較高,于是較少受開發(fā)人員青睞。但是,由于端到端測(cè)試更接近真實(shí)用戶,其在特定產(chǎn)品開發(fā)活動(dòng)中的性價(jià)比較高,有一定的發(fā)展?jié)摿Α?/p>
然而,當(dāng)前實(shí)踐中的端到端測(cè)試,普遍存在如下問題:
- 低可維護(hù)性。一般實(shí)踐并不對(duì)測(cè)試代碼質(zhì)量作特別要求,而這點(diǎn)在端到端測(cè)試就體現(xiàn)得更糟。因?yàn)槠渖婕皵?shù)據(jù)、載體、交互、功能、參照(oracle)等遠(yuǎn)比單元測(cè)試復(fù)雜的broad stack。雖然也有Page Object等模式的廣泛應(yīng)用,但仍難以應(yīng)對(duì)快速變化。
- 低運(yùn)行效率。如果拿單次端到端測(cè)試與單元測(cè)試相比,前者的運(yùn)行效率肯定更低。因此只一味增加端到端測(cè)試肯定會(huì)損害構(gòu)建-反饋循環(huán),進(jìn)而影響持續(xù)交付。
- 高不確定性。同樣因?yàn)閎road stack的問題,端到端測(cè)試有更高的幾率產(chǎn)生不確定測(cè)試,表現(xiàn)為測(cè)試結(jié)果呈隨機(jī)性成功/失敗,進(jìn)一步降低運(yùn)行效率,使得真正的問題很容易被掩蓋,團(tuán)隊(duì)也逐漸喪失對(duì)端到端測(cè)試的信心。
- 難以定位問題根因。端到端測(cè)試結(jié)果很難觸及代碼級(jí)別的錯(cuò)誤,這就需要額外人工恢復(fù)測(cè)試環(huán)境并嘗試進(jìn)行問題重現(xiàn)。其中所涉及的數(shù)據(jù)重建、用戶交互等會(huì)耗費(fèi)可觀的成本。
方法
為了解決傳統(tǒng)端到端測(cè)試遇到的種種挑戰(zhàn),本文設(shè)計(jì)了一種面向用戶旅程的端到端自動(dòng)化測(cè)試框架——雪鸮(snowy_owl),通過用戶旅程優(yōu)先、數(shù)據(jù)分離、業(yè)務(wù)復(fù)用和狀態(tài)持久化等方法,顯著提高了端到端測(cè)試的可維護(hù)性,降低不確定性的影響,并且能夠幫助團(tuán)隊(duì)成員快速定位問題。

用戶旅程驅(qū)動(dòng)
端到端測(cè)試應(yīng)盡量貼近用戶,從用戶旅程出發(fā)能保證這一點(diǎn)。在雪鸮中,用戶旅程使用被稱作play books的若干yaml格式的文件進(jìn)行組織,例如下列目錄結(jié)構(gòu):
play_books/
core_journey.yml
external_integration.yml
online_payment.yml
其中每個(gè)play book由若干plots所組成,plot用于表示用戶旅程中的“情節(jié)”單位,其基本特征如下:
- 單一plot可以作為端到端測(cè)試獨(dú)立運(yùn)行,例如發(fā)送一條tweet的情節(jié):
SnowyOwl::Plots.write 'send a plain text tweet' do
visit '/login'
page.fill_in 'username', with: 'username'
page.fill_in 'password', with: 'password'
page.find('a', text: 'Sign In').click
# verify already login?
page.find('a', text: 'Home').click
# verify already on home page?
page.fill_in 'textarea', with: 'Hello World'
page.find('a', text: 'Send').click
# verify already sent?
end
單一plot應(yīng)是緊密關(guān)聯(lián)的一組用戶交互,并且具備體現(xiàn)一個(gè)較小業(yè)務(wù)價(jià)值的測(cè)試參照。
plot可以被play book引用任意次從而組成用戶旅程,play book同時(shí)定義了所引用plots之間的順序關(guān)系,基本語法如下所示:
---
- plot_name: send a plain text tweet
digest: 2aae6c35c94fcfb415dbe95f408b9ce91ee846ed
parent: d6b0d82cea4269b51572b8fab43adcee9fc3cf9a
其中plot_name表示情節(jié)的標(biāo)題,digest和parent分別表示當(dāng)前情節(jié)引用在整個(gè)端到端測(cè)試過程中的唯一標(biāo)識(shí)和前序情節(jié)標(biāo)識(shí),初期開發(fā)人員可以通過各個(gè)情節(jié)的引用順序定義用戶旅程,大多數(shù)情況下digest和parent將由系統(tǒng)自動(dòng)生成并維護(hù)。
整個(gè)play books集合將是一個(gè)以plots為基礎(chǔ)組成的森林結(jié)構(gòu),而端到端測(cè)試的執(zhí)行順序則是針對(duì)其中每棵樹進(jìn)行深度遍歷。
通用業(yè)務(wù)復(fù)用
由于plot本身必須是一個(gè)獨(dú)立可運(yùn)行的端到端測(cè)試,那么plots之間通常會(huì)共享一部分交互操作,例如用戶登錄。雪鸮允許把高度可復(fù)用的交互代碼進(jìn)行二次抽取,稱作determination:
SnowyOwl::Determinations.determine('user login') do |name, user_profile|
# return if already login
visit '/login'
page.fill_in 'username', with: user_profile[:username]
page.fill_in 'password', with: user_profile[:password]
page.find('a', text: 'Sign In').click
# verify already login?
end
這樣,plot的代碼就可以簡化成:
SnowyOwl::Plots.write 'send a plain text tweet' do
determine_user_login({username: 'username', password: 'password'})
page.find('a', text: 'Home').click
# verify already on home page?
page.fill_in 'textarea', with: 'Hello World'
page.find('a', text: 'Send').click
# verify already sent?
end
這里應(yīng)注意Determination和Page Object的區(qū)別??此剖褂肞age Object可以達(dá)到相同的目的,但是后者與Page這一概念強(qiáng)綁定。而Determination更加側(cè)重描述業(yè)務(wù)本身,更符合對(duì)用戶旅程的描述,因此比Page Object在plot中更具適用性。當(dāng)然,在描述更低層的組件交互時(shí),Page Object仍然是最佳選擇。
測(cè)試數(shù)據(jù)分離
合理的數(shù)據(jù)設(shè)計(jì)對(duì)描繪用戶旅程非常重要,雪鸮對(duì)測(cè)試邏輯和數(shù)據(jù)進(jìn)行了進(jìn)一步分離。例如用戶基本數(shù)據(jù)(profile),同樣是使用yaml文件進(jìn)行表示:
data/
tweets/
plain_text.yml
users/
plain_user.yml
那么在plot的實(shí)現(xiàn)中,就可以使用同名對(duì)象方法替代字面值:
SnowyOwl::Plots.write 'send a plain text tweet' do
determine_user_login({username: plain_user.username, password: plain_user.password})
page.find('a', text: 'Home').click
# verify already on home page?
page.fill_in 'textarea', with: plain_text.value
page.find('a', text: 'Send').click
# verify already sent?
end
情節(jié)狀態(tài)持久化
雪鸮的另一個(gè)重要功能是情節(jié)狀態(tài)的持久化和場(chǎng)景復(fù)原。為了啟用情節(jié)狀態(tài)持久化,開發(fā)人員需要自己實(shí)現(xiàn)一個(gè)持久化腳本,例如對(duì)當(dāng)前數(shù)據(jù)庫進(jìn)行dump,并按照雪鸮提供的持久化接口把dump文件存儲(chǔ)至指定位置。
當(dāng)端到端測(cè)試運(yùn)行每進(jìn)入一個(gè)新的情節(jié)之前,系統(tǒng)會(huì)自動(dòng)執(zhí)行持久化腳本。也就是說,雪鸮支持保存每個(gè)情節(jié)的前置運(yùn)行狀態(tài)。
當(dāng)端到端測(cè)試需要從特定的情節(jié)重新開始運(yùn)行時(shí),雪鸮同樣會(huì)提供一個(gè)恢復(fù)接口,通過用戶自定義的數(shù)據(jù)恢復(fù)腳本把指定位置的dump文件恢復(fù)至當(dāng)前系統(tǒng)。
該功能有兩處消費(fèi)場(chǎng)景:
由于broad stack的問題,端到端測(cè)試不確定性的技術(shù)因素一般較為復(fù)雜。實(shí)際經(jīng)驗(yàn)表明,測(cè)試的隨機(jī)失敗率越低,就越難以定位和修復(fù)問題,而通過不斷改進(jìn)測(cè)試代碼的方式消除這種不確定性的成本較高,效果也不好。但是,可以盡量消除不確定性帶來的影響。例如,不確定測(cè)試導(dǎo)致的測(cè)試失敗,通常會(huì)導(dǎo)致額外人工驗(yàn)證時(shí)間,完全可以選擇讓系統(tǒng)自動(dòng)重試失敗的測(cè)試。另一方面,重試會(huì)造成測(cè)試運(yùn)行效率降低,特別是針對(duì)端到端測(cè)試。當(dāng)一輪端到端測(cè)試結(jié)束后,雪鸮只會(huì)自動(dòng)重試失敗的情節(jié)測(cè)試,同時(shí)利用該情節(jié)對(duì)應(yīng)的數(shù)據(jù)dump文件保證場(chǎng)景一致性,這就減少了重試整個(gè)端到端測(cè)試帶來的運(yùn)行效率下降問題。
當(dāng)團(tuán)隊(duì)成員發(fā)現(xiàn)端到端測(cè)試失敗,通常需要在本地復(fù)現(xiàn)該問題。而借助測(cè)試dump文件,可以直接運(yùn)行指定plot測(cè)試,從而避免額外的人工設(shè)置數(shù)據(jù)和交互操作,加快問題定位和解決。
實(shí)踐
雪鸮在筆者所在的項(xiàng)目有超過6個(gè)月的應(yīng)用時(shí)間。該項(xiàng)目在產(chǎn)品開發(fā)方面長期陷入困境,例如過程中同時(shí)兼具了3X每個(gè)階段的特點(diǎn),不僅缺少清晰的產(chǎn)品主線,還背負(fù)了接棒遺留系統(tǒng)的包袱。這種狀況對(duì)工程質(zhì)量管理提出了更大挑戰(zhàn)。
項(xiàng)目采用雪鸮對(duì)已有端到端測(cè)試進(jìn)行了重構(gòu),生成了一個(gè)核心用戶旅程和三個(gè)涉及外部系統(tǒng)集成的重要用戶旅程,包含24個(gè)plots,9個(gè)determinations,使端到端測(cè)試實(shí)現(xiàn)了長期穩(wěn)定運(yùn)行。在本地相同軟硬件環(huán)境下,不確定性導(dǎo)致的隨機(jī)失敗從原有10%降低至1%以內(nèi),部署至云環(huán)境并采用headless模式后,連續(xù)15天測(cè)試失敗記錄為零,運(yùn)行效率的損失可以忽略不計(jì)。同時(shí),當(dāng)用戶旅程產(chǎn)生新分支時(shí),可以引入新的情節(jié)測(cè)試節(jié)點(diǎn),并且根據(jù)業(yè)務(wù)需求將其加入現(xiàn)有play book樹,從而實(shí)現(xiàn)端到端測(cè)試的快速維護(hù)。
持續(xù)集成與常態(tài)化運(yùn)行
項(xiàng)目完整的端到端測(cè)試的平均運(yùn)行時(shí)間保持在19分鐘左右,為了不影響現(xiàn)有持續(xù)集成節(jié)奏,CI每30分鐘自動(dòng)更新代碼并運(yùn)行端到端測(cè)試,結(jié)果在dashboard同步顯示,一旦發(fā)生測(cè)試失敗,第一優(yōu)先級(jí)查找失敗原因并嘗試在本地復(fù)現(xiàn)和修復(fù)。
常態(tài)化運(yùn)行端到端測(cè)試的另一個(gè)好處是,能夠以低成本的方式實(shí)現(xiàn)24小時(shí)監(jiān)控系統(tǒng)各個(gè)組件的功能正確性,有助于更早發(fā)現(xiàn)問題:一次,產(chǎn)品即將上線的支付功能發(fā)生異常,查看CI記錄發(fā)現(xiàn)端到端測(cè)試在晚上9:15左右出現(xiàn)了首次告警。通過及時(shí)溝通,確認(rèn)是海外團(tuán)隊(duì)在當(dāng)時(shí)擅自改動(dòng)了支付網(wǎng)關(guān)的一個(gè)配置,造成服務(wù)不可用的問題,并迅速解決。
結(jié)論與展望
Kent Beck的3X模型,提出了從不同產(chǎn)品開發(fā)階段看待工程實(shí)踐的新視角。而敏捷一貫推崇的TDD等實(shí)踐,更多體現(xiàn)在個(gè)人技術(shù)專長(Expertise)方面,與產(chǎn)品是否成功并無必然聯(lián)系。然而,程序員的專業(yè)主義(Professionalism)的確同時(shí)涵蓋了技術(shù)專長和產(chǎn)品成功兩個(gè)方面,二者相輔相成。因此,如何通過平衡眾多因素并最終提高整體專業(yè)性,這才是軟件工程面臨的經(jīng)典問題。本文給出的測(cè)試三明治模型,目的就是幫助思考產(chǎn)品開發(fā)過程中測(cè)試層級(jí)間的平衡問題。
為了應(yīng)對(duì)現(xiàn)有端到端測(cè)試面臨的挑戰(zhàn),本文設(shè)計(jì)并實(shí)現(xiàn)了一種新的面向用戶旅程的端到端測(cè)試框架,通過職責(zé)隔離、業(yè)務(wù)復(fù)用和狀態(tài)持久化等手段,構(gòu)建了易于維護(hù)且更加有效的端到端測(cè)試。同時(shí),基于上述方法構(gòu)建的測(cè)試代碼,更易于和自動(dòng)化測(cè)試的其他研究領(lǐng)域相結(jié)合,在諸如測(cè)試數(shù)據(jù)構(gòu)建、用例生成、隨機(jī)測(cè)試和測(cè)試參照增強(qiáng)等方向有進(jìn)一步的應(yīng)用潛力。