單元測試的藝術-摘要

fantasy1022
10 min readJun 17, 2018

--

出處:http://www.books.com.tw/products/0010765689

之前在博客來的新書介紹看到這本書,看了一下目錄和工具介紹,書中示範的程式碼是用C#,工具介紹的工具我都沒用過,就想說算了。最近公司有買這本書就拿來翻翻看,還真的和Bob大叔說一樣XDD,看到一些我大概知道但又不是很確定的內容,可能是之前從程式碼中學習,但沒有很了解用法,為什麼要這樣設計。

不管你是對單元測試或是測試驅動開發的新手,還是已經有豐富經驗的人,都能在這本書裡找到適合自己的內容。 — — 《無瑕的程式碼》作者 Robert C. Martin(Bob大叔)

來寫一些印象比較深和理解的內容,本身是寫Android的,平常會使用的測試框架為JunitMockito,UI層會用Espresso。這本書大部分是介紹觀念和用法,並沒有很多的程式碼,我會針對我理解的內容來寫,寫起來會比較像是筆記或是摘要的形式:

單元測試基礎

單元測試應該具備以下特質:

1.可自動化,可重複執行

2.很容易被實現

3.不是臨時性的

4.執行速度應該很快

5.執行結果應該一致

6.應該是完全隔離的

7.可以被任何人執行和存取

什麼是優秀的單元測試?一個單元測試是一段自動化的程式碼,這段程式會呼叫被測試的工作單元,之後對這個單元的單一最終結果的某些假設或期望進行驗證,單元測試幾乎都是使用單元測試框架進行撰寫的。撰寫單元測試很容易,執行起來快速,單元測試可靠,易讀,並且很容易維護,只要產品程式碼不發生改變,單元測試的結果是穩定一致的。

並提到了整合測試是對一個工作單元進行測試,而這個測試對被測試的單元並沒有完全的控制,而是使用該單元一個或多個真實依賴的相依物件,例如物件、網路、資料庫、執行緒、亂數產生器。

單元測試 和 整合測試

忘記這張圖在哪邊看到了,真的是很有趣的比喻,單元測試做完還是需要整合測試來確保整體的功能,想到高品質程式這本書介紹到的測試三角型。不過本書的焦點還是和書名一樣著重在單元測試的部分。

第一個單元測試

書中提到大部分的讀者所做的測試都有某些限制:

  1. 非結構化測試
  2. 無法重構執行
  3. 測試沒有覆蓋到重要的部分

這時候就需要測試框架的幫助了,各種語言可能都有屬於自己的xUnit,Java的是JUnit,.NET的是NUnit,Haskell的是HUnit。

單元測試框架的功能

測試方法的名稱名也是很有學問的,測試方法名稱分為三個部分:

1.UnitOfWorkName:被測試的部分,一組方法或一則類別

2.Scenario:測試進行的假設條件,ex:登入失敗、無效的使用者

3.ExpectedBehavior:在測試情境指定的假數條件下,你對被測試方法行為的預期,行為有三種,回傳值、系統狀態改變、呼叫外部第三方系統提供服務。舉例來說這個命名: isValidLogFileName_BadExtension_ReturnFalse(),要測試這個 isValidLogFileName這個方法,在BadExtension的條件下,會回傳false,這樣看測試方法就可以一目瞭然,測試是在測什麼。

一個單元測試包含了三個行為(3A):

1.準備(Arrange)物件:建立物件,進行設定

2.操作(Act)物件

3.驗證(Assert):某件事符合預期

書中的參數化測試

使用參數化來進行測試,傳入各種條件和預期結果,可以測試個多邊界條件,但也不用為了測試邊界,而寫很多很像的code。

Setup和Teardown為每個測試方法前後(分別)執行的動作。作者偏向使用工廠模式來初始化物件,而不是使用Setup,Teardown也很少用到,用到可能是有相依資料庫,或是Singleton的狀態。作者認為Setup和Teardown會讓可讀性變得更差,因為單看一個測試方法時,可能會不知道準備(Arrange)物件這個動作到底做了什麼,還要回去看Setup寫了什麼。不過自已用起來是覺得習慣後就知道去哪邊找,也要看整個團隊寫測試的習慣。

盡量不要使用ExceptException,因為不知道是在哪邊發生Exception,有可能是建構的時候發生的,推薦使用Assert.Catch直接驗證會有較準確的結果,而且發生Exception後,後面寫的驗證(Assert)條件式也不會繼續執行。在JUnit也有相對應的寫法

透過虛設常式解決依賴問題

外部依賴(external dependency)是指系統中的一個物件,與被測試程式碼互動,但你卻無法掌握( 執行緒、記憶體、時間),上述提到ExpectedBehavior的其中一種(系統狀態改變)。

虛設常式(stub):是系統中產生一個可控的替代物件,來取代一個外部相依物件,可在測試過程中,透過虛設常式來避免直接相依物件造成的問題。和mock類似,但會對mock進行驗證,不會對虛設常式進行驗證。

抑制測試(test-inhibiting:當程式碼依賴於外部資源,程式碼邏輯正確,但這種依賴還是有可能導致測試失敗,可以加入一層中介層來解決。

1.找到被測試物件所使用的介面或API

2.把介面的底層實作替換成你能模擬的

需要更多的步驟:

1.找到導致被測試的工作單元無法順利測試的介面

2.如果被測試的工作單元是直接相依這個介面

3.將這個相依的介面的底層實作內容換成可以控制的程式碼

虛設常式用途

重構設計以提升程式碼的可測性,重構是在不改變程式碼的功能的前提下,修改程式碼的動作。接縫(seam)是指在程式碼中可以抽換不同功能的地方,ex:使用虛設常式類別,增加一個建構函式參數,把一個方法改成可供使用虛擬方法,透過實作開放封閉原側,對擴充開放,不能直接修改功能內部實作。

可以使用依賴注入:在被測試單元中注入一個假的實作內容

1.在建構式中得到一個介面的物件,儲存到field

2.在屬性的get或set方法中得到一個物件

3.透過以下方式,在測試前獲得一個介面的假物件方法的參數(參數注入)、工廠類別、區域工廠

使用模擬物件(Mock)驗證互動

測試與相依物件之間的互動,有三種:基於值、狀態與互動的測試,盡量選擇前兩種,互動測試會讓事情變得複雜。

互動測試是針對一個物件下如何向其他物件發送訊息(呼叫方法)的測試,如果一個特定的工作單元的最終結果,是呼叫另外一個物件,就要進行互動的測試模擬物件事系統中的假物件,可以拿來驗證被測試物件是否如預期般呼叫這個物件,因此來使得單元測試執行成功或失敗,通常每個測試只會有一個模擬物件。

假物件是通用的詞,可以用來當虛設常式物件(沒有互動),模擬物件(驗證物件間的互動),兩者最根本的差異:虛設常式物件不會導致測試失敗,而模擬物件可以。

虛設常式物件(Stub)
模擬物件(Mock)

由上圖中的介紹,就可以清楚地知道兩種物件在測試扮演的角色與功能。

隔離(模擬)框架

可用來幫助寫程式的API,使用這套API來建立假物件,比手刻物件快、容易,像是Mockito,就可以很容易建立假物件。

準備-執行-驗證(arrange-act-assert),準備階段,建立和設定假物件,接著對被測試產品執行動作,最後在結尾的部分驗證在測試中是否正常的與假物件進行互動。

使用隔離(模擬)框架來幫助測試

隔離框架的優缺點

1.更容易驗證參數

2.更容易驗證一個方法被多次呼叫

3.更容易建立假物件

缺點:

1.可讀性變差

2.驗證了錯誤的東西

3.一個測試有多個模擬物件

4.過度指定的測試

測試階層和組織

使用繼承類別繼承模式,支持物件導向程式設計最有利的理由之一是:你可以重用現有的功能,不必在別的類別一次又一次的重新設計,DRY(Don’t Repeat Yourself),設計基本類別:

1.重用輔助方法和工廠方法

2.不同類別上執行同一組測試

3.使用共用的Setup和Teardown

直接截書裡面的總結XD

好的單元測試的支柱

應該具備下列三項特色:1.可信賴的 2.可維護的 3.可讀性

具可維護性的設計來使用SetUp方法

有些Bad smell代表可能有些問題:

1.強制的測試順序:測試框架不能保證測試執行順序

2.存在呼叫其他測試的隱藏動作

3.共享狀態損壞

4.外部共享狀態損壞

一個驗證語句拋出例外,測試方法中其他程式碼就不再往下執行了,必須保證所有的驗證都必須被執行,可以改進的方式:

1.每個驗證建立一個單獨的測試

2.使用參數化測試(可讀性較好,相比try catch)

3.把驗證的程式碼放在一個try catch區塊中

物件比較,不使用多次驗證,而是建立一個用來比較的物件,物件要複寫equal才能比較,複寫toString輸入有用的資訊。

避免過度指定:

1.指定驗證被測試物件的純內部行為:測試的程式碼不屬於任何公開契約或介面

2.在需要虛設常式物件時,使用模擬物件

3.不必要的順序或過於精準的參數匹配:使用contain比equal好

Setup和Teardown 常常被濫用,由測試方法自己直接初始化建立模擬物件,設定所有的預期結果,可讀性會較好。

在組織中導入單元測試

看到這篇覺得蠻特別的,除了教怎麼做測試,還教怎麼應用到工作團隊上,如果團隊中只有一個人有寫測試,那測試的覆蓋率不會高,效果也不會好,那要怎麼說服其他人,看完覺得比寫測試還難XDD

之前有聽過說寫測試,那就不就不用QA啦,想一想就覺得不可能,測試是寫測試的人針對他預期的結果去寫,如果預期的結果就是錯的,那怎麼測都還不對,充其量也只是把事情做對,而不是做對的事。更不用說一些有使用者介面(像手機APP),複雜的操作情況了,必須要有手動的方式來進行測試。

單元測試提供了抵抗bug的第一層防護,QA工作提供了第二層:使用者驗收層。

遺留程式碼

這篇看了比較沒有共鳴,本身還沒有對很古早的code寫過測試XD,有找到一篇[遛書]《單元測試的藝術》測試可行性表,針對這張做比較細部的介紹

設計與可測試性

單元測試具有下列一些特色:

1.執行速度快

2.相互隔離,即每個測試可以獨立執行,可以按照任何順序

3.不需要額外外部的設定

4.產生穩定可靠的測試結果,包含通知和失敗

測試設計指南

看完這本書釐清了一些測試的觀念和名詞,還有心法,以及如何在應用在職場上,覺得值得買一本來收藏!

--

--

fantasy1022
fantasy1022

No responses yet