開發人員必備的技能——單元測試
說起軟體測試四個字,想必大家腦海中浮現的有整合測試、系統測試、黑盒測試、白盒測試等,可能就是沒想到會有單元測試。 對於大學是學習軟體工程專業出身的同學來說可能會聽過這四個字,對工作好幾年的職場老鳥可能也聽過但是沒實際用過居多。絕大多數的開發人員都是忙於把手頭的工作開發好,並不會把單元測試納入工作範疇,他們會說,我連功能開發都忙不過來了,哪有時間去做單元測試,況且還要寫測試程式碼,那不是重複寫一篇程式碼功能嗎?但,單元測試真的不值得花時間去做嗎,那是因為可能你並不清楚單元測試的投入產出比有多高,下面就簡單介紹單元測試到底能給開發人員帶來多少好處。
- 什麼是單元測試
- 為什麼要做單元測試
- 不寫單元測試藉口
- 主流框架 JUnit 和 TestNG
- Android 中的單元測試
- 小結
什麼是單元測試
單元測試本質上也是程式碼,與普通程式碼的區別在於它是驗證程式碼正確性的程式碼。可簡單做個定義:單元測試是開發人員編寫的、用於檢測在特定條件下目的碼正確性的程式碼。
軟體開發天生就具有複雜性,沒人敢打包票說自己寫的程式碼一點問題都沒有,或者不經測試就能保證程式碼正確執行,可能你在這個執行路徑下能夠執行,殊不知還有其他路徑,有一一去驗證過嗎,因此,要保證程式的正確性就必須要對我們程式碼進行嚴格測試。
舉個簡單例子:比如有個計算類,裡面有個 add 方法,操作就是兩個數進行相加。
public class Calculator { public int add(int one, int another) { //只是簡單的兩個數相加 return one + another; } }
常規做法:假如你寫好了這個方法,你想進行驗證 add 方法的正確性,需要寫個使用 add 方法的 main 函式,首先例項化 Calculator 類,然後呼叫 add 方法並傳入兩個引數,比如 1 和 2。然後你執行這個工程,看得出結果是否為 3 ,如果是 3 ,則表明我這個方法寫的沒有錯誤,可能就不測試了,就繼續開發後續的功能,如果不是 3 ,則返回去看看程式碼中哪裡出錯了,重新進行除錯,甚至有時候肉眼還看不出程式碼哪裡出錯,此時就引入斷點去檢視,在此期間,很大一部分時間就花在斷點、除錯、執行上。
單元測試做法:首先會利用 JUnit 測試框架(至於這個框架後面介紹)寫一段測試程式碼,如下:
public class CalculatorTest {
public void testAdd() throws Exception {
Calculator calculator = new Calculator();
int sum = calculator.add(1, 2);
Assert.assertEquals(3, sum);
}
}
這裡的 CalculatorTest 是 Calculator 對應的測試類,這裡的 testAdd 對應著 add 的測試方法,進行測試一般分為三步驟:
- setup。一般是 new 出你要測試的那個類,比如: Calculator calculator = new Calculator();
- 執行操作。一般是呼叫你要測試的那個方法,獲得執行結果: int sum = calculator.add(1, 2);
- 驗證結果。驗證得到的結果跟預期中是一樣的: Assert.assertEquals(3, sum);
看到 Assert 這個關鍵詞了嗎,這裡可以理解為斷言或者期望值,根據入參的值,期望有個什麼值輸出,而不是靠肉眼去驗證是不是自己想要的值,是直接通過判斷值是否相等性來驗證會具有更客觀性。
以上介紹的只是單元測試一點點,那它能給我們帶來哪些更多好處呢?
為什麼要做單元測試
通常我們在做任何工作會先考慮它的回報,編寫程式碼更是如此。如果單元測試的作用不大,沒有人會願意再寫一堆無用的程式碼,那麼單元測試到底能夠給我們帶來什麼優點呢?如下:
- 便於後期重構。單元測試可以為程式碼的重構提供保障,只要重構程式碼之後單元測試全部執行通過,那麼在很大程度上表示這次重構沒有引入新的BUG,當然這是建立在完整、有效的單元測試覆蓋率的基礎上。
- 優化設計。編寫單元測試將使使用者從呼叫者的角度觀察、思考,特別是使用TDD驅動開發的開發方式,會讓使用者把程式設計成易於呼叫和可測試,並且解除軟體中的耦合。
- 文件記錄。單元測試就是一種無價的文件,它是展示函式或類如何使用的最佳文件,這份文件是可編譯、可執行的、並且它保持最新,永遠與程式碼同步。
- 具有迴歸性。自動化的單元測試避免了程式碼出現迴歸,編寫完成之後,可以隨時隨地地快速執行測試,而不是將程式碼部署到裝置之後,然後再手動地覆蓋各種執行路徑,這樣的行為效率低下,浪費時間。
等等,講了這麼多優點,無非就是良好的介面設計、正確性、可迴歸、可測試、完善的呼叫文件、高內聚、低耦合,這些優點已經足以讓我們對單元測試重視起來了,但是個人覺得還有更重要的原因。
- 首先,帶來自信。在接手一個新的專案,或者說是參與一個新的專案開發時,往往這種情況是你半途參加進去的,你需要對已有的程式碼結構進行解讀和理解,對於業務的理解,對於程式碼箇中各個模組關係的理解。如果一開始就理財出錯,很可能修改後的程式碼會引起更多的BUG出現,到那時候又需要修復更多的BUG,改了一個地方,很有可能會莫名其妙地影響另外一個地方,這種現象是很常見的。還有一種情況,假設你修改的功能沒問題,但是需要去測試驗證,在測試的時候就需要考慮這個功能點它原有的測試路徑有哪些,又需要一一去驗證功能路徑,以證明本次修改對於已存在的功能點不造成影響。這其中就存在著很大的時間成本,導致效率不高。那是否存在著這麼一種方式,我需要修改我想改動的地方,不需要關心修改完之後它所造成的影響,也不需要關心它的測試迴歸性,有,此時就是單元測試登場的時候。寫單元測試程式碼,可以讓我自己寫的程式碼足夠自信,它是經得起考驗的。
- 其次,更快反饋。對於有一定程式設計經驗的開發人員來說,當他拿到一個新需求的時候,首先想到的不是動手 Coding ,而是會先想想程式碼的結構,有些類,資料結構該是如何,然後才開始敲程式碼。如果沒有單元測試,一般流程基本是這個模組功能全部寫完才開始測試,比如利用 MVP 架構的功能。一般都是開始 Model 模組,然後完善 Presenter 模組,最後寫 View 模組,等這幾個模組都寫完了,再把 APP 跑起來,驗證自己寫的功能模組是否符合需求,沒有符合則繼續回去修改程式碼,這中間需要花費很長的時間才能知道當下自己寫的程式碼是否符合要求,是否正確。那有沒有一種即時反饋的方式呢,有,寫單元測試即可,當你寫完一個函式,馬上就匹配一個單元測試函式,這樣即寫即測的方式可以保證你當場寫的程式碼馬上進行修改,測試通過一個,就表示完成一個小的功能點,最後,把函式組裝起來,就是我們想要大的功能點。
- 最後,節約時間。對於 Android 開發來說,一遍一遍的執行 APP ,然後執行相應的使用者操作,看介面是否正確的顯示,通過這種方式來測試功能,其實是非常浪費時間,而且效率不高,而用單元測試,可以幾乎不用開啟 APP 來執行,當然有些需要一些資原始檔的是需要 APP 執行條件,絕大部分的功能在單元測試階段就能驗證完畢,那麼速度就相對快很多。此外,單元測試還能幫忙減少 BUG ,從而減少除錯 BUG 的時間,一些低階犯的錯誤在單元測試階段就能避免掉。
不寫單元測試藉口
很多開發人員不寫單元測試,最重要的一個原因是他們並不知道單元測試能夠帶來什麼好處,甚至根本不瞭解單元測試這個詞,那自然就像平行線般與之毫無交集。還有一個比較重要的原因是一些開發人員的程式設計思想還處在一個相對初級的階段,開發軟體只管實現功能,什麼高內聚、低耦合、重構、設計、可測試等認為太過專業,對於這些名詞以及意義還不瞭解,這自然不會考慮使用了。還有一些非思想層面的理由,如下:
- 單元測試太花時間了。軟體開發工作那麼忙,程式碼都寫不完哪有時間寫單元測試。這可能是開發人員用的最多的藉口,從某些方面來說,這不能算藉口,因為很多開發人員確實在工作上投入的時間特別多。但真的是這樣的嗎,你有沒有想過,導致加班的原因也許就是花了太多時間在手動測試、除錯程式上:或許你沒有考慮到靈活性與設計,使得在需求發生變更時你需要花很多時間在複雜的程式碼堆中完成特定的功能,而這些修改又可能引入新的 BUG ,又將導致你需要進行耗時的手動測試、除錯等等,如此反覆,程式碼將變得越來越亂,越來越難以維護,最終導致無休止的加班。
- 測試不是我的工作。測試確實不是開發人員的工作,但單元測試確實是開發人員的工作,測試包含很多種,而只有單元測試是開發人員的工作範疇。開發人員為應用編寫程式碼,那麼自然需要保證程式碼的正確性,而單元測試正是這種保證程式碼正確性的白盒測試,也就是在瞭解程式碼內部結構邏輯的情況下進行有目的的測試,既然說到了解程式碼,那麼開發者自然是最權威的人。因此,編寫單元測試並且為測試人員提交正確的程式碼進行其他測試是開發人員的職責所在。
- 程式碼都編譯通過了,還測什麼。一般來說,這是一個不會放在嘴上但可能藏在心裡的藉口。程式碼編譯通過只能說你寫的程式碼符合語法要求,並不代表能保證正確性。
- 程式碼原來就沒有單元測試,並且難以測試。這個問題基本是接受和維護別人開發的程式碼,而原來的程式碼本身就沒有單元測試了,再加入如果程式碼的耦合性較高,那麼就更難以為這些程式碼寫單元測試。此時正是你瞭解程式碼時候,首先為能夠測試的部分新增單元測試,保證這些可測試的部分不會被汙染,然後在對程式碼有足夠的瞭解之後再對程式碼進行重構,降低程式碼的耦合性,並且慢慢補充測試用例,使得程式碼的耦合性、可測試性慢慢建立起來。
主流框架 JUnit 和 TestNG
JUnit 是一個 Java 語言的單元測試框架,它是 xUnit 單元測試架構體系的一個例項,用於編寫和執行可重複的測試。它包括以下特性:
- 用於測試期望結果的斷言(Assertion)
- 用於共享共同測試資料的測試工具
- 用於方便的組織和執行測試的測試套件
- 圖形和文字的測試執行器
TestNG 是一個測試框架,其靈感來自 JUnit 和 NUnit ,但引入了一些新的功能,使其功能更強大,使用更方便。TestNG 消除了大部分的舊框架的限制,使開發人員能夠編寫更加靈活和強大的測試。 因為它在很大程度上借鑑了Java註解( JDK5.0 引入的)來定義測試,它也可以顯示如何使用這個新功能在真實的Java語言生產環境中。
特點如下:
- 註解
- TestNG 使用 Java 和麵向物件的功能
- 支援綜合類測試(例如,預設情況下,不用建立一個新的測試每個測試方法的類的例項)
- 獨立的編譯時測試程式碼和執行時配置/資料資訊
- 靈活的執行時配置
- 主要介紹“測試組”。當編譯測試,只要要求 TestNG 執行所有的“前端”的測試,或“快”,“慢”,“資料庫”等
- 支援依賴測試方法,並行測試,負載測試,區域性故障
- 靈活的外掛 API
- 支援多執行緒測試
Android 中的單元測試
因為 JUnit 測試框架是基於 Java 語言,當然 Android 開發也是基於 Java 語言,所以在 Android 中我們可以用 Junit4 單元測試框架進行迴歸測試,但同時,Google 也提供了一個 AndroidJUnit4 測試框架,看名字就知道它是基於 JUnit 4 框架適合在 Android 環境中做單元測試。
那麼,AndroidJUnit4 和 Junit4 有什麼區別呢?很大一個區別在於:
1,AndroidJUnit4 測試可以在真機的環境下進行。比如你要測檔案讀取SD卡,或者操作 SqlLite 資料庫,這些條件只有在真機上才有的,此時你用 AndroidJUnit4 框架測試,可以直接跑起來用真實的環境做相應的單元測試。
2,JUnit4 測試是執行在工程專案中,也就是在編譯階段。此時如果想要模擬 Android 環境,比如我想用 JUnit4 來測試 Activity 類,那麼就需要引用第三方庫來支援,引用 Mockito 和 Robolectric 框架來模擬 Android 環境進行相應的單元測試。
所以何時用 AndroidJUnit4 和 JUnit4 不同的框架進行單元測試,就看你待測試的方法前置條件是什麼,然後做不同的選擇。
小結
總的來說,單元測試不是整合測試,單元測試只是測試一個方法單元,不是測試一整個流程。整合測試是一種End To End的系統測試,測試相關模組整合在一起是否能夠按照預期工作,一般都是介面或者功能層面的測試,可能會依賴很多系統因素,測試的程式碼邏輯一般比較複雜,執行時間會比較長,出錯之後的修復成本高。單元測試則是開發者在整合測試之前就已經進行自測過,同時呢,進行單元測試之後,對於某個方法的執行路徑組合進行了一一驗證,它只關注三個目標:
- 有明確的返回值。比如對某個函式進行單元測試,驗證其返回值是否符合預期結果。
- 這個函式只改變其物件內部的一些屬性或者狀態,函式本身沒有返回值,就驗證它所改變的屬性和狀態。
- 一些函式沒有返回值,也沒有直接改變哪個值的狀態,這就需要驗證其行為,比如點選事件。