單元測試的理解


 首先聲明以下大部分是摘錄。

原則定的都很好,是不是真的能做到?一切看起來都很美,一切聽起來都對,在做的時候是不是真的落實了?

先來講一個單元測試的故事

單元測試寫出來容易跑過難!而且跑不過的原因還不是你的開發代碼邏輯錯了,而是測試環境/數據出問題。要測試,一定要有數據,這個數據的構建,完全不是我們所想象的那么簡單。以我們項目里的積分系統為例,假設一個簡單的需求:博客被點贊,博客的作者應該獲得一定積分,該積分數量是由點贊人目前所有的可用幣轉換而得來的。要准備的數據就有:博客一篇,要有作者,作者已有積分;點贊人一名,有一定數量可用幣。如果只是這樣,還可以接受,但其實下面會有一堆的問題:

 

  • 作者的積分從哪里來?我們的開發代碼,出於封裝的考慮,用戶的積分是只讀的,你單元測試怎么設這個值?
  • 要么寫代碼,模擬作者通過其他行為(發布文章回答問題等)獲得積分,這將開啟新一輪噩夢;
  • 如果用Mock或者反射強行設置,事實上省略了作者獲得積分的歷史,所以用戶“積分歷史”為null,之后對其“加積分”時,就會報異常。
  • 更坑的是,你以為你什么都處理好了的時候,你突然悲哀的發現,這個博客得首先“被發布”,而博客一經發布,其作者就獲得了一定數量的積分,所以你以前設置的積分又變了!
  • ……
  • 點贊人的可用幣,同樣可能遇到類似的問題。可用幣怎么設置,設置之后會不會在跑測試時被意外更改?
  • 點贊的行為,被封裝成一個方法,運行這個方法,會檢查點贊人之前是否已經對該文章點過贊,所以還應該有一個“點贊歷史記錄”,哪怕是空的,都得new一個,否則就空異常
  • ……

 

反正當時是寫得我直接摔了鼠標!寫得憋屈啊!而且我還是完全隔絕了數據庫的,真不知道那些要從數據庫里取數據來跑單元測試的,是怎么做的?這時候我一下子就明白了,實際工作中加班趕進度,一個接一個的填坑,連重構的時間都沒有,怎么可能還擠得出時間來寫單元測試?就算開始雄心勃勃的寫了,隨着系統日益復雜,維護單元測試的成本也與日俱增,甚至復雜度更甚開發,所以放棄也就成了絕大多數項目的唯一選擇。

 這個故事聽起來就是一個從入門到放棄的例子。有很多的細節點都值得深思。

  1. 如何對某些模塊獨立測試,屏蔽相關項?
  2. 數據庫操作怎么屏蔽?
  3. 變更需求測試代碼也變更,造成大量工作量

 所以,今天來講講單元測試的相關基礎知識及原則,自己對於單元測試的理解。

 

單元測試是什么?

單元測試是針對軟件的最小模塊進行正確性檢驗的測試工作。

什么是測試用例?

A test case has components that describes an input, action or event and an expected response, to determine if a feature of an application is working correctly.

哪些代碼需要做單元測試?

所有的代碼都需要單元測試.

所有公共(public)的代碼都需要單元測試.

單元測試是程序員還是測試負責?

單元測試是由程序員自己來完成,最終受益的也是程序員自己。可以這么說,程序員有責任編寫功能代碼,同時也就有責任為自己的代碼編寫單元測試。執行單元測試,就是為了證明這段代碼的行為和我們期望的一致。

為什么要使用單元測試

單元測試會為我們的承諾做保證。編寫單元測試就是用來驗證這段代碼的行為是否與我們期望的一致。有了單元測試,我們可以自信的交付自己的代碼,而沒有任何的后顧之憂。

  • 單元測試使工作完成的更輕松
  • 單元測試使你的設計更好
  • 大大減少花在調試上的時間
  • 能幫助你更好的理解代碼

如果沒有單元測試

  • 任何代碼都是在假定其他代碼是正確無誤的情況下編寫的。
  • 修改一處代碼時無法得知會對其他代碼產生怎樣的影響。
  • 任何一處改動都需要進行功能級別的整體調試。

什么是Mock,Stub,Spy?

(sinon.js對以下概念的定義)

Spy的作用在於可以監視一個函數的被調用情況。函數會被實際調用,Spy相當於給函數加了一層wrapper,可以記錄函數被調用了幾次,每次的參數是什么,每次返回結果是什么,出了什么異常。

Stub的作用是在測試中遇到這樣的情形:測試函數f1,f1依賴於函數f2,根據最小化測試粒度的原則,測試f1時不要帶入f2的測試,那么我們要確保f2的輸出是正確的,所以直接讓f2返回正確值的方法就叫stub。測試時,stub的方法是不會被具體執行的。

Mock是對於一個對象的監視,並且需要定義對這個對象的具體期待(verify),然后驗證這些期待是否和測試數據一致。

在sinon.js等測試工具中,mock對象是不會被真正執行的。spy是真正執行的。

在其他一些測試工具中,對以上概念有所變化和混淆。例如mockito中,mock對象后,具體方法也可以定義返回值,作用等同於stub。

 

 單元測試的原則

1、每次只對一個對象進行UT測試(unit-test one object at a time)。這樣能使你盡快發現問題,而不被各個對象之間的復雜關系所迷惑。

2、給測試方法起個好名字(choose meaningful test method names)。應該是用形如testXXXYYY(),這樣的格式來命名你的測試方法。前綴test是Junit查找測試方法的依據,XXX應該是你測試的方法名,YYY應該是你測試的狀態。當然如果你只有一種狀態需要測試可以直接命名為testXXX()。

3、明確寫出出錯原因(explain the failure reason in assert calls)。在使用assertTrue,assertFalse,assertNotNull,assertNull方法時,應該將可能的錯誤的描述字符串,以第一個參數傳入相應的方法。這樣你可以迅速的找出出錯原因。

4、一個UT測試方法只應該測試一種情況(one unit test equals one testMethod)。一個方法中的多次測試,只會混亂你的測試目的。

5、測試任何可能的錯誤(test anything that could possibly fail)。你的測試代碼不是為了證明你是對的,而是為了證明你沒有錯。因此對測試的范圍要全面,比如邊界值、正常值、錯誤值;對代碼可能出現的問題要全面預測。

6、讓你的測試幫助改善你的代碼(let the test improve the code)。測試代碼永遠是我們代碼的第一個用戶,所以不僅讓他幫組我們發現Bug,還要幫組我們改善我們的設計,就是有名的測試驅動開發(Test-Driven Development,TDD)。

7、一樣的包,不同的位置(same package, separate directories)。測試的代碼和被測試的代碼應該放到不同的文件夾中,建議使用這種目錄 src/java/代碼 src/test/測試代碼。這樣可以讓兩份代碼使用一樣的包結構,但是放在不同的目錄下。 

8、關於setup與teardown 

a) 不要用TestCase的構造函數初始化Fixture,而要用setUp()和tearDown()方法。

c) 當繼承一個測試類時,記得調用父類的setUp()和tearDown()方法。 

9、不要在mock object中牽扯到業務邏輯(don’t write business logic in mock objects)。 

10、只對可能產生錯誤的地方進行測試(only test what can possibly break)。如:一個類中頻繁改動的函數。對於那些僅僅只含有getter/setter的類,如果是由IDE(如Eclipse)產生的,則可不測;如果是人工寫,那么最好測試一下。

11、盡量不要依賴或假定測試運行的順序,因為JUnit利用Vector保存測試方法。所以不同的平台會按不同的順序從Vector中取出測試方法。

12、避免編寫有副作用的TestCase,你要確信保持你的測試方法之間是獨立的。 

13、將測試代碼和工作代碼放在一起,一邊同步編譯和更新(使用Ant中有支持junit的task)。

14、確保測試與時間無關,不要依賴使用過期的數據進行測試。導致在隨后的維護過程中很難重現測試。

15、如果你編寫的軟件面向國際市場,編寫測試時要考慮國際化的因素。不要僅用母語的Locale進行測試。 

16、盡可能地利用JUnit提供地assert/fail方法以及異常處理的方法,可以使代碼更為簡潔。

17、測試要盡可能地小,執行速度快。

 

單元測試最佳實踐

實踐一: 三到五步

  • SetUp
  • 輸入
  • 調用
  • 輸出
  • TearDown

實踐二: 運行快速

為什么?

單元測試運行很頻繁,是輔助開發的,在開發過程中運行,如果慢影響很大

多快較好?

  • 單個測試小於200ms
  • 單個測試套件小於10s
  • 整個測試小於10分鍾

實踐三:一致性

任何時候同樣的輸入需要同樣的結果

    Date date=new Date() Random.next()

這樣的代碼都需要Mock掉,不然時間每次都不同,結果就會不一樣。

實踐四:原子性

** 所有的測試只有兩種結果:成功和失敗**
不能部分測試通過

實踐五:單一職責

一個測試只驗證一個行為

** 測試行為,不要測試方法 **

  • 一個方法,多個行為    ----->  多個測試
  • 一個行為,多個方法   -----   一個測試
    這里的一個行為,多個方法一般指這個方法調用private, protected, getters, setters
  • 多個Assert只有在測試同一個行為時可以接受

實踐六:獨立無耦合

單元測試之間無相互調用

  • 單元測試執行順序無關
  • 不同的順序無影響

單元測試之間不能共享狀態

比如一個測試里設置了一個屬性值,然后在另外一個測試里用,如果必須共享可以放到Setup里

實踐七:隔離外部調用

  • 單元測試需要快速運行,且每次結果一致,所以需要隔離一切對外部的調用。
  • 不使用具體的其它真實類,就是不要new
  • 不讀數據庫
  • 不讀網絡
  • 不讀外部文件
  • 適當時候可以構造一個相同的內部文件來Mock
  • 不依賴本地時間
  • 不依賴環境變量

實踐八: 自描述

  • 單元測試是開發級文檔
  • 單元測試是方法的描述

實踐九: 單元測試邏輯

  • 單元測試必須容易讀和理解的
  • 變量名,方法名,類名
  • 無條件語句,無Switch
    辦法:分解if到多個測試,所有的輸入都是已知的,所有的結果都是一定的(Mock)
  • 無循環語句
  • 無異常捕捉
    ** 測試預知的異常,用ExpectedException方法 **

實踐十: 斷言

  • 斷言信息最好包含Business Information
  • 斷言信息包含出錯的具體信息如果失敗
  • 適當時候可以封裝自己的Assert
    比如:

    Assert.IsProgrammer(Jack)
    Return Jack. Cancooking() && Jack.CanCoding()

實踐十一:產品代碼

  • 產品代碼無測試邏輯
    不能有:

  If(global.IsTest){…}

    • 測試代碼和產品代碼要分離
    • 不要在產品代碼里有任何只供測試用的代碼
    • 使用依賴注入

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM