【單元測試】單測編寫技巧與原則
最近因工作需要不得不對單元測試中的Mockito2和Powermock框架的一些新特性進行研究:比如Mockito2和Powermock可以偽造靜態方法、final類甚至是建構函式的呼叫,但是研究一段後發現,這些功能其實在我本來就很熟悉的Jmockit框架中就能實現,而且不用像mockito一樣需要特殊的語法和額外的樣板程式碼,看似掌握了一些所謂“高大上”的用法,實際對工作來說沒有任何收益。因此今天這篇文章不會講什麼單測框架的高階特性,反而,我們來聊一聊指導我們進行單元測試的一些基本準則。
mock還是不mock?
為什麼我們需要mock來進行單元測試呢?為什麼我們需要用一些假物件來替換我們測試類中的真物件呢?原因是我們想讓自己的單元測試是密閉獨立的,實際測試時,任何一個類都有可能依賴於其他的類,這些依賴可能來自於同一原始碼的同一根目錄,可能來自一些核心庫(java.util.ArrayList, java.io.File),更有一大部分來自於第三方庫。或許這些依賴的輸出是穩定且我們可以預期的,但實際生產環境中,它們可能會依賴於檔案系統、網路等這些變幻莫測的外部資源,比如任何一個使用當前日期/時間或讀取硬體資源的物件,其結果對我們來說都是不可預知的,而這樣的不可預知,對於單元測試來說,就是最大的bug。
在單元測試中,我們需要保證除了我們要測試的類,其“外部世界”的行為與輸出與我們所預期的完全一致。舉個栗子,比如我們需要測試一個service,這個service作用是根據各國的區號,計算當前國家的稅率。
double taxRate = TAXService.getTAXRateForCountry(countryCode);
比如在單測case中,我們假設美國的稅率是21%,但是除非我們ctrl+滑鼠點選進去TAXService這個類中去檢視,我們無法得知是否真的是我們所預期的21%。有可能TAXService類依賴於本地檔案,也有可能會去伺服器中去查,而查詢本地檔案,或者連線伺服器這一動作,就大大降低了我們單元測試的效率。單元測試的目的就是為了快速地測試這段程式碼的可用性,如果想要確保整體應用於預期一致,我們要做的並非單元測試,而是整合測試、端對端測試、壓力測試等。。。
那麼是否所有的case都通過mock來寫比較好呢?
如果是下面的例子呢:
// 非mock的寫法
String postCode = employeeDao
.getEmployeeById(employeeId)
.getPreviousEmployer()
.getAddress()
.getPostCode();
// mock的寫法
when(employeeDao.getEmployeeById(42)).thenReturn(employee);
when(employee.getPreviousEmployer()).thenReturn (previousEmployer);
when(previousEmployer.getAddress()).thenReturn(address);
when(address.getPostCode()).thenReturn(“1234AB”);By Jove, we’re finally there!
哪個更簡單明瞭,已經不言而喻了吧。
註釋還是不註釋?
在編寫單元測試程式碼時,為了使讀程式碼人的人清除這部分的case測試點在哪裡,很重要的一部分是編寫註釋,有效的註釋與漂亮的程式碼同樣重要。這裡要強調的是“有效“,在一些時候,過多無用的註釋對檔案和對你本身也是一種累贅。
如果你的程式碼不言自明,那就別註釋了。比如下面這個例子:
// 過濾關鍵字
for (String word: word) {...}
// 基本稅金
int taxmoney = ...;
// 扣除稅費減免
finalTax = (taxItems * taxPrice)
- min(5, taxItems) * itemPrice * 0.1
下面是幾個有效註釋的例子:
1、解釋你的意圖:解釋程式碼為什麼這麼做,而不是做了什麼
// 最終稅金 = 稅金 - 減免金
2、做好todo:,避免其他人“修復”了你的程式碼
// TODO: 優化稅費計算公式,保證小數點後3位
3、做好問題澄清,避免在code review時別人的誤解
// 使用這個順序計算稅率的原因是。。。
寫不寫before?
請看下面這個例子:
private final Tax tax = new Tax();
Before
public void setUp() {
tax.increment("key1", 8);
tax.increment("key2",100);
tax.increment("key1",0);
}
//1000行之後
@Test
public void testIncrement_existingKey()
{
assertEquals(9, tally.get("key1"));
}
除非再把頁面拉回去,我們無法得知1000行之後的單測case的結果是否正確。抽象點來說,就是原因與結果相隔太遠了。
我比較傾向的單測case的寫法,是把原因與結果放在一個case中,像我們說話一樣,開始為原因,結束為結果,這樣的好處是程式碼可讀性更強,維護更簡單,後續同學的使用也更方便。
private final Tax tax = new Tax();
@Test
public void testIncrement_newKey() {
tax.increment("key", 100);
assertEquals(100, tax.get("key"));
}
@Test
public void testIncrement_incrementByZeroDoesNothing() {
tax.increment("key", 8);
tax.increment("key", 0);
assertEquals(8, tax.get("key"));
}
結語
其實,做單元測試的意義就在於能使測試人員得到更好地測試覆蓋率,使團隊得到更高的產出效率,使研發人員更自信地進行重構,如果我們寫的單元測試既臃腫又不好維護,那做單測的意義還何在呢?