近年來,TDD(測試驅動開發)的方法在程式設計圈子裡大行其道,許多程式設計者陸續開始採用這種以測試活動為出發點的設計方法。

不過,在前些日子,Ruby on Rails 之父 David Heinemeier Hansson(DHH)在今年的 RailsConf 會議中,宣稱 TDD 已死,甚至在會議的演講隔天,寫了一篇「TDD is dead. Long live testing」的文章,宣稱 TDD 已死。這種戰文等級的文章自然引來各方爭論交戰,可以說是各執一詞、各有各的道理。

本文並不是要加入這場爭戰,而是想討論另一個和程式測試相關的議題。因為,即使是 DHH 宣稱 TDD 已死,他還是認為,測試很重要(Long live testing)。的確,即使你不打算採用 TDD 的開發方式,測試這項工作,還是在開發過程中扮演了舉足輕重的角色。

測試在軟體開發過程中的重要性:有助於找出Bug,及早修正

我們已經反覆強調過製造出臭蟲、除去臭蟲的代價很高,同樣的,無法找出程式中所隱藏的臭蟲,代價一樣很高。而有效率的測試工作是減少這些成本的方法。

有了好的測試方式,就可以更有效率地找出程式臭蟲,進而降低除去臭蟲的時間。因此,倘若能改善測試所耗費的成本,就能進一步改善軟體開發時的整體成本。

那麼,究竟是那些因素在影響著測試的成本呢?從系統的架構、設計、甚至程式碼的實作,都和測試的成本相關。

在軟體工程裡,有一個名詞叫做「可測試性(testability)」,它代表的是軟體(包括元件及系統)本身因為設計、實作的特質,因而可用最佳的成本效益比來模擬出應用情境,以及可被自動化測試,以達到更多的涵蓋率的能力。

應用的情境,代表軟體所實際執行時外在環境可能會有的各種情境變化,當你在測試時,難以製造出各種可能的情境時,你就難以讓軟體在各種情境下皆進行測試,以驗證軟體是否正確無誤。同樣的,如果無法運用自動化測試達到更多的測試涵蓋率時,那麼,代表有更多的涵蓋案例,無法自動化、快速進行。

軟體開發時,提升程式碼本身的可測試性,有助於改良設計

簡單來說,「可測試性(testability)」就是軟體是否容易被測試的能力。

當你在設計你的軟體時,不論是架構、還是程式碼模組間的互動,或是實際撰寫程式碼時,你想過如何讓你的程式更好測試嗎?

如果你採用 TDD 之類的開發方法,每次在撰寫程式碼前先撰寫好測試程式,那麼或許在不自覺間,為了讓單元測試碼更好撰寫,你會間接讓真正的程式碼更容易被測試。不過,這二者之間不存在必然的關聯,也就是說,即使你不用 TDD ,你所下的程式碼,也可能有高度的可測試性,反之亦然,TDD 不代表程式一定會有夠高的可測試性。

無論採用那一種開發方法,提升程式碼的可測試性顯然是能帶來好處的。因此,程式設計者應該關心該怎麼做,可以提高自身程式碼的可測試性。而且,程式碼如果具有好的測試性,可以降低每次測試的成本、提高測試的涵蓋率,使得每次測試時讓臭蟲顯現的能力變好。

一般都認為,可測試性和設計的好壞程度高度相關。也就是說,我們一般所認知的一些好的設計原則,隱含著有更好的能力帶來可測試性。

例如,好的設計都會有高聚合、低耦合的特性,也就是模組化程度高,另外,也會有低冗餘性。因此,若將心力放在如何讓設計更好,所製造出來的產物,就包括了好的可測試性。這是我們之所以應該做出更好設計的原因之一。

或許你可以解讀從 TDD 出發,有機會因為讓程式碼好測試而運用了好設計。相反的,若是從好設計的方向出來,也會帶來好的可測試性,條條大路通羅馬。

掌握四大設計要點,提高軟體的可測試性

那麼要怎樣才能提高可測試性呢?

軟體的測試架構師 David Catlett 提出了一個名為 SOCK 的模型,S 指的是 Simplicity (單純性)、O 是 Observability(可觀察性)、C 是 Control(控制)、 K 則是 Knowledge of the expected result(對預期結果的知識)。從這種特性著手,就可以提高軟體的可測試性。

S──單純就是不複雜。愈複雜的軟體愈難以測試,這個道理十分直覺。倘若軟體模組之間高度的相依,而且其中一個模組狀態的變化就足以造成其他模組行為的改變,可以說是牽一髮動全身時,那麼想要測試這整個系統,難度就會很高。

例如,當你的某個類別內部狀態很多,而每個函式的執行結果又取決於這些內部狀態,那麼為了測試這些函式是否正確,就會需要更多的測試才足以涵蓋。這都說明了,設計單純有助於可測試性。

O──可觀察性,所代表的是軟體可被觀察的能力,這是什麼意思呢?若從測試程式的角度來看,測試程式是一個外部的角色,它需要透過某種途徑來觀察並得知受測軟體的狀態,用以判斷測試究竟是成功或是失敗。觀察受測軟體的狀態,其重要性不只在於取得結果,還包括了取得執行中間的狀態,以便判斷程式究竟會怎麼執行分支,也才能判斷程式應有什麼結果。

例如,取得的室內溫度超過 28 度就必須要自動開啟冷氣,而系統是否有開啟冷氣,可以透過讀取某個狀態信號而得知。那麼,測試程式就可以透過讀取目前室內溫度以及系統是否開啟冷氣的信號來檢驗系統是否正確。當程式的可觀察性愈好,透過程式手法可以觀察到的程式狀態就愈多,可以自動測試的程度就愈高。

而C──控制。代表的則是可以用測試程式來控制受測程式的能力。當你可以用測試程式動態的控制一些外在因素,使得受測程式的行為因而隨之產生對應的改變時,你會更容易對受測程式進行自動化的測試。

有些程式的執行路徑,是在特殊的條件下才會執行,例如,室內溫度為 28 度下才開啟冷氣。但測試進行時,若要製造各種不同的溫度,以測試低於 28 度、高於 28 度下的程式反應,對測試來說就增加了許多困難。但是,若能由測試程式去控制 28 度的這個門檻值,事情就簡單多了,而且是具有可確定性的,例如分別設定一個絕對不會發生的溫度門檻值,以及一個絕對會發生的溫度門檻值。單元測試時,運用所謂的 Mock Object ,即為一種好的控制能力。

最後是K,即對預期結果的了解。如果不知道某個功能的執行結果應該為何,又如何自動化的驗證它的確正確呢?此外,程式的預期結果是否容易驗證其對錯也很重要,倘若程式的結果不容易區分結果是否為正確,也會增加測試程式的難度。

不論你是否採用 TDD,能自動化的測試程式總是好事。而更重要的是,能被自動化測試的程式,通常具備了好的設計特質,而好的設計特質也能帶來好的可測試性。因此,對程式設計者來說,你值得花點心思想一想,和了解各種增加可測試性的方式和手法。

專欄作者


熱門新聞

Advertisement