前言
1. 學習態度
過去當我遇到新知識時,我會問自己一個問題:“這個東西有很多人學嗎?”,沒有的話我就不學。
但是現在回想一下,這種想法實在是不太理智了,難道股神巴菲特在投資股票時,會考慮這是不是一隻熱門股票嗎?
他不會,因為真理是掌握在少數人手裡的。
我在《把時間當作朋友》中看到這麼一個道理:
這個世界上有兩種人,一種人是不知道這個知識有什麼用,所以決定不學習。
另一種人則是不知道這個知識有什麼用,所以決定去學習,隨著時間的推移,兩者的認知差距會越來越大。
看了這段話後,我就調整了一下自己的心態,對於新知識,我不再抱著懷疑的態度,而是抱著學一學、試一試,說不定真的有用的態度。
這篇文章的內容不是熱門知識,但是學習了這門知識後,讓我的開發水平有了很大的提升,所以我想分享給大家。
2. 目標讀者
如果你正在找工作,那這篇文章可能不適合你,因為這不是能讓你在面試時拔高的內容。
之所以這麼說,是因為就我的瞭解而言,大部分公司的開發者並不關心這個內容。
大部分的開發者的態度是這樣的:
“什麼?測試?那不是測試人員做的事情嗎?質量是測試人員負責的,我只負責實現。”
當然了,這只是說我接觸過的公司和人,不是所有開發者都是這樣的,比如我知道的實踐單元測試的公司就有 Google 和 ThoughtWorks 。
有很多需求的實現的確是很有挑戰的,比如抖音的各種特效。
但是大部分需求的實現是很簡單的,所以在大多數時候我們都要考慮如何提升實現的質量。
如果你已經進入工作了,並且對於開發過程中,不斷返工修 Bug 的現象感到非常沮喪,那這篇文章就是為你準備的。
3. 工作經歷
兩年前,我在一家創業公司工作,進入那家創業公司後,覺得雖然公司只有幾個人,但是他們都有大公司背景,工作態度都很積極。
尤其是後臺,是個研究生,在騰訊和搜狗都做過,而且也創業過,他參加創業的那家公司,甚至做到了上億的流水,很牛。
當時我就在想,說不定我進入這家公司後,很快我們公司就會像新聞上的創業公司一樣,一年就融到 1 個億,三年就上市,很快我就能迎娶白富美,走上人生巔峰。
然後我就工作非常努力,加班,沒問題,通宵加班,也沒問題。
但實際情況是這樣的,老闆之前已經招過幾個安卓開發了,個個都是巨坑。
上一任的 Android 開發好像在華為做過,有很多年的工作經驗,聽上去很厲害的樣子,實際上也是個大坑。
這裡說的坑,指的是開發出來的 App 有各種 Bug ,連基本的使用都有問題,更別說什麼使用者體驗了。
這次我進來,老闆期待我能帶給公司帶來新希望,但我帶來的不是新的希望,而是新的絕望。
我開發出來的 App 各種崩潰,怎麼點怎麼崩,那些修好了這裡崩。
當時沒有測試人員,開發出來的東西直接交給老闆驗收,老闆看了之後也快崩了。
當時我們幾個人針對這個問題開了好幾次會,並沒有找到什麼解決方案,我自己也很糾結、很痛苦,但是當時懵懂無知的我並不知道怎麼辦。
在掙扎了一段時間後,我離開了這家公司,離開後我就一直在想,難道這真的是軟體開發的宿命嗎?
難道軟體開發就只能是不斷修 Bug 嗎?那為什麼微信幾乎都沒有 Bug ?
後面我就圍繞這些問題看了很多本書,最後發現這並不是軟體開發的宿命,我遇到的問題在幾十年前就已經有人遇到了,而且這個坑已經被他們填上了。
前人已經提出了很多填坑的方法,其中一個方法就是這篇文章的主題:
測試驅動開發(TDD, Test-Driven Development)
如果把對知識的運用分為“不知道—知道—做到—做好”這四個水平的話,那麼我在 TDD 上 ,也只達到了“做到”的水平。
而寫這篇文章的其中一個原因,就是希望自己能通過重新回顧、學習這些知識,讓自己進一步靠近“做好”這個水平。
除了找到返工問題的答案以外,這段工作經歷還讓我明白了另一個道理:
不要老想著我需要什麼,多想下我能提供什麼。
4. 內容概覽
接下來的內容會分為 TDD 入門和 TDD 示例兩個部分來講。
-
TDD 入門
1.1 排雷
1.2 軟體內部質量
1.3 TDD 週期
1.4 避免迴歸
1.5 小結
-
TDD 示例
2.1 基於斷言的測試
2.2 被動檢視模式
2.3 三個基本準則
2.4 基於互動的測試
文中的程式碼在文章的最下方會有 GitHub 連結。
1. TDD 簡介
我是一個非常粗心、編寫程式碼時考慮問題非常不周到的一個人。
在一次工作經歷中,我遇到了一位思維比我嚴謹很多的 iOS 開發者。
當時我問了他一個問題:為什麼你能想到我想不到的事情呢?
他說:這都是經驗,等你專案做多了,你也能想到的。
而我想的是,怎麼讓一個沒什麼經驗的人,在寫程式碼時也能做到周全地考慮問題?而 TDD 就是這個問題的答案。
TDD 用一句話說就是:
寫程式碼只為修復失敗的測試。
有了測試作保障,我們可以逐步改進程式碼的結構,寫出可讀、可測試的程式碼、避免過度設計。
而且不用擔心優化程式碼會破壞已有功能,導致軟體迴歸。
迴歸,指的是軟體的功能回到了以前的狀態,比如一個功能本來是可以用的,一改就用不了了。
1.1 排雷
TDD 中的測試,不是指手工測試,不是指用手對著 App 點點點,而是指單元測試。
軟體開發中的單元測試,和我們學校裡的單元測試是非常相似的。
為什麼這麼說呢?
學校裡的單元測試的目的,就是為了驗證我們是否真的理解並記住了書上的知識。
而軟體開發中的單元測試的目的,則是為了驗證我們是否真的理解我們的程式碼。
可能大家聽到這裡會覺得很奇怪,什麼?程式碼是我自己寫的,我怎麼可能不理解?
下面給大家看一個例子。
可能你認為上面這個函式的執行路徑是這樣的。
但實際上它可能的執行路徑還有另外兩條。
一個這麼簡單的函式,都能有兩個“雷”等著我們排,如果是一個幾百行的函式,就更不用說了。
而 TDD 就是一個幫助我們思考程式可能的執行路徑的方法,是一個幫助我們“排雷”的方法。
我前面說到了我之前做的應用怎麼點怎麼崩,為什麼呢?就是因為這樣那樣的判斷我沒有加上。
剛剛提到的這兩個低階錯誤,是大多數程式中的大多數函式都可能出現的錯誤。
只要我們能把這些“雷”都排掉,我們應用即使達不到零 Bug 的水平,穩定性也能有巨大的提升。
1.2 軟體內部質量
雖然軟體開發行業已經發展了幾十年了,但是大多數開發者開發出來的軟體依然有非常多的質量問題。
這裡說的質量分為兩種,一種是外部質量,一種是內部質量。
外部質量,指的是軟體對與需求的符合程度。
內部質量,指的是軟體內部程式碼的健壯性、可讀性、可維護性。
關於提升軟體外部質量的方法,會在我的下一篇講 BDD 的文章中進行介紹講到。
在這一篇文章,我們主要看下怎麼提升內部質量。
使用 TDD ,我們要寫很多小型的自動化測試,這些測試最終會形成一個有效的預警系統,防止軟體出現迴歸的情況。
這些測試就像是煙霧報警器,能讓我們在“火災”蔓延之前進行滅火。
TDD 能讓我們寫出質量更高的程式碼,把過去用於打斷點、跑斷點、不斷除錯的時間,用在實現新功能和優化程式碼上。
使用 TDD 後,當測試失敗時,因為只有最近寫的幾行程式碼才有可能破壞測試,所以我們能在更短的時間內找到問題。
假如執行一次程式要 2 分鐘,1 個斷點等待 10 秒鐘,找到對應的變數又是 10 秒鐘,那麼斷點多的時候,這個過程估計就是十幾分鍾,一不小心打錯斷點了,就要重來一次。
如果 Bug 多了,需要不斷除錯,那我們就不用開發新功能了。
如果我們能快速完成新功能,返工修 Bug 的次數很少,甚至是零返工,那麼我們就可以花更多的時間學習新技術、優化我們的程式碼,形成一個良性迴圈。
在軟體內部質量上的主要兩個問題是:缺陷多、維護難。
1. 缺陷多
軟體缺陷,也就是 Bug ,會導致軟體不穩定、行為不可預測、完全無法使用,甚至讓軟體帶來的破壞遠超過創造的價值。
如果一家餐廳做出來的菜裡有蟑螂,那問題很大概率是在廚房,而不是在服務員身上。
但是在軟體行業,當做出來的軟體有 Bug 時,大家卻很有可能把矛頭指向測試人員(服務員),而不是開發人員(廚師)。
但問題的根源在軟體的內部,軟體的外部行為是由內部的一個個函式相互呼叫而產生的結果。
只有這一個個函式都是健壯的時候,軟體的外部行為才有可能按預期工作。
那什麼是健壯呢?
有的時候我會聽到一些開發者說這樣的話:那是後臺給的資料有問題,我的應用才會閃退的。
又或者是:那是前端提交的引數有問題,才會提示異常的。
之所以他們會說這樣的話,估計是並不瞭解軟體健壯性的定義:
軟體的健壯性,指的是在異常和危險情況下,系統生存的能力。
比如輸入引數有誤、磁碟故障、網路過載以及有意攻擊等行為下,能否不當機、不崩潰。
說白了就是它可以空,你不能崩。
而建立單元測試,用各種方式給我們自己寫的函式“找茬”,就可以提升這些函式的健壯性。
傳統的測試方法,是在需求開發完成後,再進行黑盒測試。
黑盒測試,就是測試是在不瞭解軟體的內部工作機制的情況下進行的,比如手工測試、使用 Selenium 等自動化測試工具測試。
黑盒測試的問題就在於有很多內部的“雷”,光靠外部的點點點是點不出來的,因為這些類往往是在資料異常的時候才會“爆炸”。
有 Bug 的軟體是不能交付的,我們在尋找和修復 Bug 上投入的時間越多,也就意味著我們的開發能力越低。
比如計劃用 10 天開發一個模組,結果中途遇到了非常多的 Bug ,修 Bug 花了 5 天,最後開發出來花了 15 天甚至更長的時間。
2. 維護難
只有寫出可維護性高的程式碼,我們才能迅速響應業務需求的變化。
好的程式碼有很多優點,比如良好的設計、各個部分的功能和職責都是清晰的、沒有冗餘、重複的程式碼。
而 Bug 通常是由低質量的程式碼引起的,維護這些程式碼、基於這些程式碼進行擴充套件簡直是一場噩夢。
比如重複的程式碼會讓 Bug 的修復變得困難,改完一個地方,還要改其他 4、5 個甚至更多的地方。
當專案中充斥著爛程式碼時,我們按時交付的壓力會越來越大,導致我們寫出質量更差的程式碼,形成惡性迴圈。
1.3 TDD 週期
一般開發流程是:
設計—實現—(手工)測試。
而 TDD 流程是:
建立測試—編寫程式碼—重構程式碼。
也就是先建立單元測試、編寫實現程式碼讓測試通過,然後再對實現進行重構優化。
1.3.1 TDD 週期
下面我們來看一下 TDD 的具體步驟。
1. 建立測試
TDD 週期也叫“紅—綠—重構”,當我們做第一步建立測試時,測試會失敗。
測試失敗,表明系統不具備我們期望的功能,而 IDE 會用紅色表示失敗的測試,比如下面這樣。
第一步是建立測試而不是編寫生產程式碼,是因為這樣可以提高我們程式碼的可用性。
在還沒有寫生產程式碼前,我們能以使用者的身份看這個函式好不好用,不用考慮這個函式好不好寫。
就像是產品人員在根據需求設計功能時,可以暫時忽略技術可行性,把全部精力用在思考怎麼讓使用者用得更爽。
又比如我們客戶端開發者,對於後臺提供的介面好不好用會很敏感,後臺要求的引數會不會太麻煩,後臺返回的欄位好不好用,我們都能夠快速給出反饋。
但是當面對我們自己寫的程式碼時,我們往往會變成了當局者的身份,只考慮到怎麼實現比較容易,而不是怎麼實現比較好用。
有的人甚至會把自己寫的程式碼,和自己的尊嚴關聯在一起。
如果被測試人員找出了問題,而且很不給面子的說出來了,就感覺下不了臺。
如果我們把自己的身份從當局者轉變為旁觀者的話,我們就能更理性、更客觀地看待自己的程式碼,從而更好地找出實現可能存在的問題。
另外在建立測試時,我們要注意建立的測試粒度要小,要寫“剛好失敗”的測試。
而不是一下子寫出整個模組的測試,然後花幾天寫程式碼讓測試通過。
當你熟悉建立測試的方法後,一般建立一個測試需要的時間在幾秒鐘到幾分鐘之間,編寫生產程式碼的時間也應該在這個時間區間內。
如果我們編寫測試程式碼或生產程式碼的時間超過了這個區間,那說明測試的粒度太大了, 要把測試範圍和生產程式碼中的函式進行拆分。
2. 編寫程式碼
而第二步編寫程式碼,就是為了讓測試從失敗的狀態變為通過的狀態,這時 IDE 會用綠色來表示測試結果,比如下面這樣。
TDD 的第二步是編寫程式碼,編寫“剛好能通過測試”的程式碼。
新建立的測試失敗表明系統缺少了預期的功能,我們應該只投入幾分鐘就能實現這個功能,測試失敗的狀態不應該持續太久。
TDD 的基本理念是讓測試指出下一步該做的事情,使用這種方式比在我們的程式碼裡嵌入 // TODO 要更清晰,每一次通過測試,我們都知道工作取得了進展。
在這一步我們的目的是讓測試儘快通過,所以不要投入太多時間尋找最佳實現。
等功能實現、測試通過後,我們可以再回過頭來優化這個實現。
這麼說的前提是功能容易實現,而且產品人員提出的功能大部分都是容易實現的。
如果遇到了一個比較難實現的功能,需要很長時間來實現,而且更換實現非常複雜,那就要先考慮好如何實現,否則返工的時間會讓我們吃不消的。
3. 重構優化
而最後一步重構,也就是優化現有程式碼的結果,改變程式碼內部結構的同時,由於有測試的保護,我們不會破壞程式碼的外部行為。
重構本身是一件很危險的事情,如果優化程式碼的帶來的是使用者投訴,那還不如不優化。
如果有測試作保障,我們不僅能對程式碼進行優化,把技術債給還上,而且也不會破壞使用者體驗。
重構是一種用於改進設計、消除重複的開發方法,通過持續重構可以逐漸提升軟體的內部質量。
重構優化是 TDD 週期的最後一步,在這一步我們回過頭來審視現有的程式碼結果,並找出更好的實現。
使用 TDD 而不進行重構,會導致迅速產生大量的劣質程式碼、技術債,無論我們的測試有多充分,都還不上這些技術債。
在用測試驅動、小步前進的過程中,我們會不斷擴充套件當前的設計,以便支援新功能的開發,也會不斷拋棄舊的概念。
而這種編碼方式一不小心就會變成“修修補補”,最終把程式碼的邏輯搞得自己也看不明白。
想了解更多重構手法的話,可以看 Martin Fowler 大神的書:《重構》。
1.4 避免迴歸
如果想保證從專案開發的第一天起就能交付軟體,就要不斷重構程式碼,而且還要保證重構後程式碼的外部行為沒有被破壞。
在沒有人監督、驗證我們開發的軟體時,我們可能會偷懶,懶得去驗證自己的實現是否正確、是否有漏洞。
TDD 中測試不僅能幫助我們設計和開發軟體,還可以督促我們以正確的方式進行開發,避免出現偷懶的情況。
手工測試非常麻煩,所以開發人員甚至是專業的測試人員都不喜歡做測試,會跳過某些功能的驗證,自以為地認為某個功能應該沒問題。
而解決這個問題的方法,就是把手工測試轉化為自動化測試。
在自動化測試中,測試套件就像是一個模具,能套進去的程式碼就是正常、可工作的程式碼。
而當我們修改測試程式碼或生產程式碼,破壞了測試套件或生產程式碼的功能後,也就表明軟體出現了“迴歸”的情況。
而為了測試軟體是否出現了迴歸情況的測試,就叫回歸測試。
迴歸測試如果是由手工來執行,會非常麻煩非常複雜,效率非常低,是開發週期中佔了非常多時間的一部分工作。
如果能把這部分工作自動化,就能在減少很多測試時間的同時,提升迴歸測試的質量。
1.5 小結
-
使用 TDD ,通過自己給“找茬”的方式,我們能把程式中大部分的“雷”都排掉。
-
TDD 的三個週期分別是建立測試、編寫程式碼和重構優化。
要注意的是建立的測試粒度要小,最初的實現不需要是最好的實現。
-
使用 TDD 能快速地執行迴歸測試。
當我們建立了測試集後,測試集就能像煙霧報警器一樣為我們工作,讓我們能在“火災”發生前就把火撲滅。
2. TDD 示例
2.1 基於斷言的測試
常見的兩種測試方式是基於斷言的測試和基於互動的測試,我們先來看基於斷言的測試。
2.1.1 第一個斷言
1. 建立測試
假設我們現在有這樣一條業務規則:手機號必須要是 11 位的,否則要有錯誤提示。
我們接下來根據這條業務規則來建立一個測試。
下面是用 Kotlin ,以 MVP 的形式建立的登入頁,首先建立的是 Presenter 的測試類。
我們在這裡可以看到,由於 LoginPresenter 類不存在,AS 提示編譯失敗。
這裡的 assert() 是 Kotlin 提供的一個斷言方法,接收的引數是布林值,當 assert() 方法接收到的引數為 false 時,就會報一個 AssertionError 。
assert() 是基於 JUnit 的斷言 API 進行封裝的,JUnit 的使用比較簡單,在這裡就不多說了。
2. 通過編譯
下一步就是解決這個編譯時錯誤,建立 LoginPresenter 類。
3. 執行測試
由於我們剛才並沒有做真實的實現,而是直接返回 true ,所以執行測試的結果肯定是失敗的。
之所以在明知會失敗的情況下,還執行測試,是因為失敗的測試結果是一種反饋,是有意義的。
就像是你沒做過蛋炒飯,然後你想嘗試一下,結果很難吃,這也是一種反饋。
有了反饋,你就可以不斷地調整你的做法(實現),最終達到好吃的程度(目標)。
2.1.2 第二個斷言
當我們把 isPhoneValid() 中的返回值改為 false 後,testIsPhoneValid() 就能通過測試了。
但這還不能代表 isPhoneValid() 是有效的,我們需要用更多引數測試這個方法。
在下面可以看到,由於把返回值改為硬編碼的 false ,所以現在失敗的是第 29 行。
下面我們用真實的實現替換掉原有的硬編碼,替換後,測試就通過了。
2.1.3 質量底線
有的朋友可能遇到的情況是時間上不允許做這件事,比如上級說專案這個星期要上線,我不管你怎麼弄,你只要上線就行了。
那這時候是不是就應該放棄質量呢?
在我看來不是的,因為有坑的程式碼上線後,有多少坑你就要背多少鍋。
使用者體驗被破壞,意味著我們的工作從為使用者創造價值,變成了給使用者帶來麻煩。
一名有職業素養的開發人員,應該堅守質量底線,而且一家不顧產品質量的公司,是不可能有競爭力,不可能有什麼長遠發展的。
如果是一兩次那很正常,但是如果長期是這樣,首先要爭取跟上級溝通,說明其中的利害關係。
實在不行,就應該考慮換一家公司。
當你堅守質量底線後,換來的就是一個易於維護、易於擴充套件的專案。
也就是接下來你不用再投入大量的時間“救火”,可以把時間用於開發新功能、學習新技術上,從而提升後續的開發效率。
2.2 被動檢視模式
1. 被動檢視簡介
在 1996 年,Taligent 公司提出了經典 MVP 模式。
在最初的設計中,View 會把接收到的使用者輸入事件轉交給 Presenter 處理。
同時 View 還會用觀察者模式監聽資料的變化,當 Model 發生變化後,View 會自動更新自己的狀態。
後續基於經典 MVP 模式衍生了另外兩個 MVP 模式的變體:
- 監督控制者(Supervising Controller)模式
- 被動檢視(Passive View)模式
其中被動檢視模式是使用最多一種,而它流行的主要原因是因為可測試性強。
在被動檢視模式中,View 要把使用者輸入事件傳遞給 Presenter,View 不能與資料層有關聯,不需要自動更新檢視,而是聽 Presenter 的吩咐。
這句話是什麼意思呢?就是說你不能在 Activity 中讀取 SharedPreferences 、資料庫,甚至連時間什麼的資料也要由 Presenter 給 Activity 。
2. 測試效率
可能有的朋友看了上面說到的測試步驟後,會覺得好麻煩,這麼多步驟,還要寫這麼多程式碼。
還不如我直接在 Activity 裡面判斷,一下就搞定了,這麼做效率太低了。
但是如果把“效率”放在更長的時間段來看的話,你就會發現直接在 Activity 裡面判斷的效率才是最低的。
如果放在 Activity 的話,就意味著要在真機上執行 App ,或者是用 Robolectric 沙盒模擬 Android 環境。
和直接在本地的 JVM 上執行比起來,這兩種方式都比較耗時。
尤其是打包和安裝 APK 的過程,會導致測試成本會大幅提升,測試效率會大幅下降。
而如果我們把對手機號的判斷獨立地放在一個函式中,我們就可以在本地的 JVM 上執行測試,快速地驗證函式的有效性,快速“排雷”。
假如專案中現在有一個函式,這個函式沒有註釋,命名也不清晰,寫了幾百行。
而以前寫這段程式碼的人已經離開了,那現在你要以什麼為依據進行重構呢?
哪怕這個函式只有幾行,命名清晰,如果專案的其他地方發生了變更,影響到了這個函式,手工測試沒有發現異常,結果一上線就出問題了。
緊接而來的不是客戶投訴就是使用者數量下降,那這樣看哪種方式的效率更高呢?
在 TDD 中,重點測試的物件是 Presenter 中的業務邏輯。
如果把資料全都放在 Activity 了,Acitivty 自己把活都幹完了,那還測什麼 Presenter ,還分什麼層,直接全寫在 Activity 裡面完事了。
如果跳過 Presenter 直接測試 Activity 的話,就意味著你想驗證一個 if 語句都要經歷打包、安裝 APK 這個過程,測試成本會大幅提升,測試效率會大幅下降。
3. 不穩定因素
就連 Presenter 中都不能直接讀寫資料,而是要通過 Model 介面來運算元據。
因為真實資料是導致測試不穩定的一個重要因素,我們要把這部分用介面進行隔離,以便後續可以用 Mock(模擬)生成模擬資料。
為什麼說真實資料是不穩定因素呢?
假如現在需要測試訂單列表,訂單列表是真實資料,這樣的話每一個修改訂單狀態的測試,都會讓下一次的測試失敗。
結果就是測試的穩定性就非常差,每一次測試都要重置資料或調整測試程式碼。
這是我踩過的一個大坑,在剛開始實踐 TDD 的時候,我寫了大量的測試後面都報廢了,就是因為這些測試是基於真實資料的,後續執行全都失敗了。
上面只討論了 MVP,如果你用的是 MVVM ,道理也是一樣的,View 中不可以放資料,否則難以建立穩定的測試。
2.3 三個基本準則
要想提升生產程式碼的可測試性,我們在設計程式碼結構時就要注意下面三個準則:
- 使用組合替代繼承
- 避免靜態成員和單例
- 依賴注入
1. 使用組合替代繼承
使用繼承的缺陷就在於,如果現在有一個抽象的基類,這個基類我們無法例項化它。
如果我們想測試這個基類的方法,就要在它的子類中進行,但如果它有幾個子類,我們到底要在哪個子類中測試這些方法比較合適呢?
如果使用組合的話,我們就可以獨立地測試這些方法,不用糾結這個問題。
2. 避免靜態變數和單例
靜態變數和單例物件會在多個地方被使用,這就意味著哪怕它們通過測試後,也不能保證後續不會出現問題。
因為在真實環境中,到底哪裡還會修改它們的值有很大的不確定性。
儘量把金泰變數改為區域性變,這樣測試的結果會穩定很多,測試結果的可靠性也更高。
3. 依賴注入
控制反轉是物件導向程式設計的一種設計原則,可用於降低程式碼之間的耦合度,控制反轉的常見方式就是反射和依賴注入。
反射和依賴注入的區別就在於,依賴注入可以通過容器來建立例項,我們可以弄一個假的例項來替換真正的實現,具體有什麼用下面會用一個例子來說明。
2.4 基於互動的測試
前面我們看了基於斷言的測試,下面我們來看下基於互動的測試,也就是測試 Presenter 與 View 和 Model 之間的互動是否以期望的方式執行。
1. 新增依賴
使用模擬資料的優點是我們能穩定地測試 App 中的業務邏輯,但缺點就是我們無法用測試與後臺聯調。
實際上我遇到的大多數“聯調”,不過是客戶端幫後臺踩坑罷了,因為後臺人員不懂 TDD ,不知道滿足什麼條件後,介面才算是完成了。
由於完整的登入邏輯需要驗證 Presenter 與 View 和 Model 之間的互動,而我們不要真實的資料和檢視,所以我們需要用模擬物件來替換真實的實現。
在這裡我們使用 MockK 來模擬資料,MockK 是一個 Kotlin 版的 Mockito,而且比 Mockito 要強大。
關於 MockK 用法的更多介紹,可以看我另一篇文章,這裡就不詳述了。
2. 模擬響應
上是 LoginPresenterTest 中的程式碼,程式碼中的 mockk() 方法,是用於建立模擬物件的。
view 的 mockk() 方法中,多了一個 relaxed = true ,表示該模擬物件的方法有預設返回值,不需要顯式指定。
之所以不需要顯式指定 View 的方法的返回值,是因為我們在這裡不管 View 到底有沒有把活幹好,我們只管 Presenter 有沒有叫 View 幹活。
對於 View 的測試,也就是 UI 層的測試,會在我的下一篇文章 BDD 中介紹。
我們現在只需要控制資料層 Model 的響應,也就是指定 Model 方法的返回值。
在一般的情況下,我們只能被動地等待伺服器出現正常或異常的響應,而現在我們自己來製作響應。
這樣的話,不論後臺返回的是什麼資料,我們都能夠用假資料穩定地測試 Presenter 的邏輯。
我們如果在編寫程式碼的時,由於伺服器只返回了正常的資料,導致我們只可能考慮到正常路徑。
而使用模擬響應的話,我們就可以用各種亂七八糟的異常資料,驗證程式的各個執行路徑是否存在問題。
3. 建立測試
下面這段也是 LoginPresenterTest 中的方法,由於兩個測試有重複的程式碼,在這裡把重複的程式碼抽取到 testOnLogin() 方法中。
4. 實現 Presenter
下面的程式碼是 LoginPresenter 中的實現,這裡的實現比較簡單,大家看看就好了。
這裡 onLogin() 之所以用 on 開頭,是因為 View 只能通知 Presenter ,而不是叫 Presenter 幹活,在函式的命名上也要體現這一點。
而且在這裡還給 isPhoneValid() 方法加了一個 @VisibleForTesting 註解,有了這個註解,我們就可以測試這個私有函式,而 View 是無法呼叫這個函式的。
5. 檢視報告
除了直接執行測試以外,我們還可以通過 Gradle 任務執行全部測試。
test 任務執行完畢後,會生成一份測試報告。
點選 test 執行測試任務,測試執行完成後,把目錄檢視從 Android 切換到 Project,並開啟 app/build/reports/tests 目錄。
右鍵 index.html ,選擇用瀏覽器開啟。
開啟後我們就能看到一份測試結果報告。
點選包名後可以看到各個測試的測試結果和執行時間,下面是 LoginPresenterTest 的測試結果。
結語
只有實踐才能把知識轉換為切切實實的價值,我是在看了不少 TDD 的資料後,才開始實踐的。
實踐後我才發現,TDD 不僅讓開發者自己寫測試,而且還要在沒有生產程式碼之前就寫測試的方式。
TDD 這種反直覺的開發方式,因為要寫兩套程式碼,給我們的第一感覺往往是浪費時間。
但堅持這個實踐,就能讓我們能把開發效率在更長的時間跨度上提升,而不是執著於眼前的效率。
可能有的朋友看到這裡會覺得 TDD 挺簡單的,和生產程式碼比起來,大多數測試程式碼的建立的確不復雜,難的地方在於堅持給自己寫的程式碼“找茬”。