前言
斷言assert是測試框架的重要組成部分。本篇介紹斷言的各種類型,結(jié)合測試框架介紹3種斷言工具。
//TODO:自定義斷言
//TODO:預(yù)期結(jié)果
斷言簡介
在《xunit pattern》中提出了“四階段自動(dòng)化測試“,即一個(gè)最簡單的測試用例可以由如下圖所示的4個(gè)步驟組成。

斷言主要應(yīng)用在xUnit“四階段自動(dòng)化測試“中的第三步-驗(yàn)證(verify)階段。即對于執(zhí)行完成SUT某項(xiàng)指令之后,來驗(yàn)證其狀態(tài),或者執(zhí)行的結(jié)果。
斷言01- 三種斷言工具:Junit原生、Hamcrest與AsserJ比較
本小節(jié)將簡要介紹Junit原生、Hamcrest、以及AssertJ這三個(gè)不同時(shí)代的經(jīng)典斷言工具。這三個(gè)工具可以在不同類型的測試中使用。另外,很多專用自動(dòng)化測試工具,如RestAssrured等也傾向于自帶斷言。在了解了經(jīng)典斷言工具后,對于了解這些專用工具自帶的斷言也更有益處。
Hamcrest
Hamcrest 屬于”新一代”的斷言工具,Hamcrest這個(gè)單詞是”matchers”的變位詞。它提供了大量豐富的匹配器,能夠讓斷言可讀性更高,斷言樣板代碼量更小,更易維護(hù)。Hamcrest一經(jīng)問世,就取得了非常大的成功,甚至一度成為第一個(gè)被Junit引入的第三方包,成為Junit4斷言的一部分[1]。另外,Hamcrest也開發(fā)出了其他語言的版本,如C++, C#, Objective-C, Python, ActionScript 3, PHP, JavaScript, Erlang, and R。
當(dāng)然,由于以AssertJ為代表的“新新一代”斷言工具的出現(xiàn)以及蓬勃發(fā)展,Hamcrest又被移除出了Junit5[2], 讓測試框架的使用者們可以更加自由的選擇斷言工具,促進(jìn)Junit生態(tài)圈的發(fā)展。
AssertJ
AssertJ與之前的斷言工具的最大不同,是引入了流式斷言(Fluent Assertion),讓斷言的編寫更加流暢,可讀性更強(qiáng),從而讓它大獲成功。它甚至還提供了一鍵轉(zhuǎn)換的工具,將傳統(tǒng)的Junit Assert斷言轉(zhuǎn)換為AsserJ斷言。
Hamcrest與AssertJ比較
熱度
首先來比較一下Hamcrest和AssertJ的熱度。

Hamcrest在2007年7月首次登陸Maven中央庫,最近一次更新則在2012年7月,而AssertJ則在2013年登陸Maven中央庫,截至本文寫成的2018年8月,當(dāng)年度已經(jīng)發(fā)布3次。兩者均錄得了累計(jì)超過4000個(gè)項(xiàng)目的引用,在Maven中央庫的歷史排名均處于40位之內(nèi),屬于明星項(xiàng)目。順便說一下,Junit以超過7萬個(gè)項(xiàng)目的引用成為最受歡迎的項(xiàng)目,而TestNG則以6000多次的引用排在20多位,兩者差距非常明顯[3]。
簡要比較
以下是筆者整理的Junit Assert、Hamcrest以及AsserJ的簡要比較。
| 斷言工具 | 斷言種類 | 斷言語法 | 斷言類數(shù)量 | IDE自動(dòng)提示 | 軟斷言及行為 |
|---|---|---|---|---|---|
| Junit Assert | 一般 | 對象比較 | 一個(gè) | 方便 | Assume,預(yù)期不符合則用例跳過 |
| Hamcrest | 豐富 | 對象比較 | 多個(gè)斷言類 | 不方便 | 借助于Assume,行為同上。 |
| AssetJ | 豐富 | 流式斷言 | 一個(gè) | 方便 | SoftAssertions,預(yù)期不符合繼續(xù)執(zhí)行,待執(zhí)行完畢后用例失敗 |
關(guān)于AsserJ的具體使用,可以參考其官方提供的項(xiàng)目
https://github.com/joel-costigliola/assertj-examples
1 Marc Philipp (21 Oct 2012). "Summary of Changes in version 4.4". JUnit documentation. Retrieved 20 Sep 2016.
2(https://en.wikipedia.org/wiki/Hamcrest#cite_ref-3) "JUnit 5 User Guide - Third-party Assertion Libraries". Retrieved 11 May 2018. https://junit.org/junit5/docs/5.0.0/user-guide/#writing-tests-assertions-third-party
4 https://joel-costigliola.github.io/assertj/assertj-core-converting-junit-assertions-to-assertj.html
斷言02-斷言變體
除了應(yīng)用于Verify 階段的斷言,還有如哨兵斷言、delta斷言等不同的斷言形式。
1)哨兵斷言
這是一種讓測試用例快速失敗的斷言,一般存在于用例的前部,甚至是setup階段,或者是底層的測試框架中。
如何判斷需要使用這種類型的斷言呢? 當(dāng)測試用例中出現(xiàn)了if這樣的判斷來決定測試用例的執(zhí)行路徑時(shí),就需要考慮是否引入哨兵斷言了。這樣就可以在測試用例用引入測試邏輯。

典型的案例是,在UI 自動(dòng)化測試中,往往會(huì)首先判斷一下某個(gè)頁面的標(biāo)志性icon是否存在,如果存在,則繼續(xù)執(zhí)行該頁面下的操作。
另外一種場景是,在通過API接口進(jìn)行業(yè)務(wù)場景自動(dòng)化測試時(shí),我們會(huì)假設(shè)協(xié)議層通訊正常,request/response可以正常發(fā)送和接收。業(yè)務(wù)的結(jié)果,無論正確/錯(cuò)誤,都在更上層的response中體現(xiàn)。
如HTTP restful的接口,其HTTP狀態(tài)碼(HTTP Status Code)應(yīng)該都是200,表示消息傳輸正常。
因此,我們可以在測試框架的通信層首先對狀態(tài)碼進(jìn)行斷言,保證協(xié)議層的通信正常,然后再將返回的body交由上層代碼進(jìn)行處理。
一個(gè)簡單的示例如下:
@Before
public void setUp() {
RestAssured.baseURI= "http://192.168.1.119";
RestAssured.port = 8080;
RestAssured.basePath = "/service/v1";
}
@Test
public void testUserLogin() {
expect().
statusCode(200).
body(
"success", equalTo(true),
"userInfo.userId", equalTo("admin"),
"userInfo.firstName", equalTo("admin"),
"userInfo.lastName", equalTo("admin"),
"error", equalTo(null)).
when().
get("/user/login?userName=admin&password=abc");
}
//為簡單起見,該案例直接將body信息進(jìn)行了驗(yàn)證。
如果有需要,如每個(gè)用例均需要完成的哨兵斷言,甚至都可以考慮放進(jìn)setup方法中進(jìn)行,便于重復(fù)使用。
2)Delta斷言
Delta斷言讓我們有機(jī)會(huì)脫離SUT的具體狀態(tài)來進(jìn)行驗(yàn)證。
如在某個(gè)測試用例中,測試用例需要驗(yàn)證轉(zhuǎn)賬1個(gè)億的準(zhǔn)確性。因此,我們可以通過驗(yàn)證該賬戶轉(zhuǎn)賬前后的資金差異來確定結(jié)果是否準(zhǔn)確。如以下的偽代碼
@Test
public void testBalance() {
long balanceBefore=api.queryBalance();
api.trans(1,"aaa","bbb");
long delta = api.queryBalence() - balanceBefore();
assertThat(delta).isEqualto(1);
}
采取這種方式的好處是,我們可以不必要知道,或者驗(yàn)證該賬戶在轉(zhuǎn)賬前后的具體資金是什么。
如果沒有采用delta驗(yàn)證,而是直接驗(yàn)證轉(zhuǎn)賬(前)后的該賬戶資金余額,那么則要求該測試用例需要嚴(yán)格控制上下文,保證每次執(zhí)行該用例時(shí),系統(tǒng)賬戶的金額處于預(yù)期的狀態(tài)下。
@Test
public void testBalance() {
long balanceBefore=api.queryBalance();
assertThat(balanceBefore).isEqualto(123456789);
api.trans(1,"aaa","bbb");
assertThat(api.queryBalance()).isEqualto(123456788);
}
讀者一定也會(huì)發(fā)現(xiàn),這樣的用例對于系統(tǒng)的環(huán)境控制要求較高,如果該用例執(zhí)行時(shí),系統(tǒng)沒有將該賬戶余額正確設(shè)置為初始值,用例就會(huì)在第一步失敗。
或者其它用例中也用到了該賬戶進(jìn)行了轉(zhuǎn)賬/入賬的操作,并沒有及時(shí)復(fù)原(如reset數(shù)據(jù)庫)的話,由于用例間的潛在數(shù)據(jù)依賴關(guān)系,導(dǎo)致用例也會(huì)執(zhí)行失敗。
斷言03-驗(yàn)證方法
對于結(jié)果驗(yàn)證來說,至少有兩種方法可以選擇。
- 直接驗(yàn)證返回結(jié)果
如前一小節(jié)中轉(zhuǎn)賬的案例,
assertThat(api.trans(1,"aaa","bbb")).isEqualto("OK");
通過直接驗(yàn)證方法的返回值,可以對結(jié)果進(jìn)行直接驗(yàn)證。
2)間接驗(yàn)證
在前一小節(jié)的轉(zhuǎn)賬案例中,筆者通過查詢賬戶在轉(zhuǎn)賬前后的余額來對結(jié)果進(jìn)行驗(yàn)證。這種不對被測對象(轉(zhuǎn)賬接口)進(jìn)行直接驗(yàn)證,而通過間接方法進(jìn)行驗(yàn)證的方式,也是測試過程中常用的方法。
在傳統(tǒng)的帶有數(shù)據(jù)庫的系統(tǒng)中,測試人員也非常習(xí)慣于在前臺(tái)操作完成后,到系統(tǒng)數(shù)據(jù)庫中通過編寫SQL的方式進(jìn)行查詢驗(yàn)證結(jié)果。
這是因?yàn)?,一個(gè)接口的調(diào)用,除了完成返回值之外,可能會(huì)產(chǎn)生多個(gè)后續(xù)的動(dòng)作。

這些后續(xù)的操作,也可以作為驗(yàn)證的對象。
就轉(zhuǎn)賬而言,轉(zhuǎn)賬成功后,該用戶的賬戶余額會(huì)發(fā)生改變。另外,如果設(shè)置了當(dāng)日轉(zhuǎn)賬限額的話,該限額也應(yīng)該會(huì)被更新。
就系統(tǒng)自身而言,還可以延申到數(shù)據(jù)庫表的記錄更新、日志系統(tǒng)的記錄等等。
在金融系統(tǒng)中,如果涉及到了資金的變化,一般建議除了直接返回值進(jìn)行驗(yàn)證之外,應(yīng)該盡可能地通過間接驗(yàn)證地方式對系統(tǒng)進(jìn)行測試驗(yàn)證,尤其是如當(dāng)日轉(zhuǎn)賬限額等隱含更新的數(shù)據(jù)。在實(shí)際地工作中,這些也是出現(xiàn)過漏測缺陷的教訓(xùn)的。
斷言04-預(yù)期結(jié)果
這一部分主要關(guān)注驗(yàn)證(Verify)時(shí)的預(yù)期結(jié)果的問題。當(dāng)談到預(yù)期結(jié)果時(shí),經(jīng)常會(huì) 聯(lián)系到test oracle。根據(jù)維基百科,test oracle是以下這個(gè)意思:
In computing, software engineering and software testing a test oracle, or just oracle, is a mechanism for determining whether a test has passed or failed.[1] The use of oracles involves comparing the output(s) of the system under test, for a given test-case input, to the output(s) that the oracle determines that product should have.
在測試設(shè)計(jì)中,除了關(guān)于預(yù)期結(jié)果的具體內(nèi)容之外,還關(guān)心
1)驗(yàn)證結(jié)果的范圍
2)驗(yàn)證結(jié)果如何產(chǎn)生
3)可否自動(dòng)生成預(yù)期結(jié)果
測試結(jié)果獲得的復(fù)雜性比較
在UI自動(dòng)化測試中,根據(jù)筆者的經(jīng)驗(yàn),由于在界面上獲取數(shù)據(jù)的復(fù)雜性,往往會(huì)簡化驗(yàn)證范圍。如新建用戶的場景,往往只會(huì)驗(yàn)證創(chuàng)建過程的完成(如出現(xiàn)某個(gè)提示icon)或者是簡單在用戶列表中能查詢到該新建用例的用戶名,亦或者通過delta斷言比較系統(tǒng)用戶數(shù)量+1。如果通過一個(gè)頁面上的表單來逐個(gè)獲取一個(gè)用戶的10個(gè)屬性,來和預(yù)期結(jié)果進(jìn)行比對,是非常不經(jīng)濟(jì)的行為。
而在接口測試等較為底層的測試中,結(jié)果往往可以通過返回值的方式獲取到,如一個(gè)數(shù)據(jù)庫或者用戶信息接口的查詢,即可完整獲得上述10個(gè)屬性值,并和預(yù)期結(jié)果進(jìn)行比較。這也反映出了底層測試更為經(jīng)濟(jì)和高效。
全面比較的必要性和成本
由于UI自動(dòng)化中獲取數(shù)據(jù)的復(fù)雜性,測試人員經(jīng)常會(huì)選擇只對部分關(guān)鍵信息進(jìn)行斷言。而在API自動(dòng)化測試中,雖然數(shù)據(jù)的獲取成本大為降低,但是由于接口返回值的字段往往較長,人工逐個(gè)編寫預(yù)期結(jié)果也往往費(fèi)時(shí)費(fèi)力,測試人員也經(jīng)常選擇只對一些關(guān)鍵信息進(jìn)行斷言。希望既能保證測試結(jié)果的正確性,又能保證一定的設(shè)計(jì)和執(zhí)行效率。
然而在實(shí)際的測試實(shí)踐中,筆者所在團(tuán)隊(duì)也發(fā)生過因?yàn)轭A(yù)期結(jié)果不夠豐富,導(dǎo)致了某個(gè)缺陷遺留到線上的問題。后來經(jīng)過缺陷根因分析,發(fā)現(xiàn)
1)該測試場景雖然在分析時(shí)沒有考慮到,但是在設(shè)計(jì)用例時(shí),其實(shí)已經(jīng)觸發(fā)了該缺陷,或者說其實(shí)該場景已經(jīng)覆蓋到了。
2)測試人員在編寫預(yù)期結(jié)果時(shí),只校驗(yàn)了和測試場景直接相關(guān)的字段,對于返回結(jié)果中與缺陷相關(guān)的字段沒有校驗(yàn)。從而產(chǎn)生了漏測缺陷。
預(yù)期結(jié)果的動(dòng)態(tài)生成 (runtime assertion )
在之前的案例中,所有的預(yù)期結(jié)果,無論是人工編寫的,或者是通過運(yùn)行生成的,在下一次的測試用例運(yùn)行之前,這些數(shù)據(jù)都是已經(jīng)確定的。
在金融系統(tǒng)中,基礎(chǔ)數(shù)據(jù)是經(jīng)常變化的。在之前的一篇關(guān)于 數(shù)據(jù)管理的文章 中提到了動(dòng)態(tài)數(shù)據(jù)的問題。如果希望能一次編寫用例,可以在不同的基礎(chǔ)數(shù)據(jù)環(huán)境中運(yùn)行的話,就需要運(yùn)用動(dòng)態(tài)數(shù)據(jù),通過運(yùn)行時(shí)查詢和基礎(chǔ)數(shù)據(jù)衍生等方式,來生成測試用例的入?yún)⒑团c之配套的預(yù)期結(jié)果。
目前我們在線上冒煙測試系統(tǒng)上采用了這種方式。當(dāng)然,由于入?yún)⒑皖A(yù)期結(jié)果之間的關(guān)聯(lián)算法其實(shí)比較復(fù)雜的,甚至可能是業(yè)務(wù)的一種簡單實(shí)現(xiàn)。開發(fā)和維護(hù)這些算法的成本也是比較高的。這也阻礙了這種測試方法在功能測試中的大規(guī)模使用和推廣。