示例代碼太少,之後會逐漸補上。html
目錄:java
若是你查過一些關於單元測試的資料,你可能會和我同樣發現一個問題。有一些文章在說到單元測試的時候,提到了要作邊界測試,要考慮各類分支;也有一些文章則說的是修改原有代碼,例如依賴隔離;還有一些文章說的是測試的框架的使用,例如 Junit 。那麼它們之間有着什麼樣的聯繫呢?android
最開始,咱們可能更關注邊界測試和分支測試。但遺憾的是,這方面的資料相對來講較少。更多的是依賴隔離這類的文章。爲何?segmentfault
由於有不少代碼是沒法被測試的。安全
可以被測試的代碼須要知足某些條件。你可能會以爲很麻煩,作單元測試還要爲了知足這些條件去修改原來的代碼。事實上,知足這些條件能使你的代碼變得健壯。若是你寫的代碼是沒法被測試的,那麼你的首要任務就是將它們重構爲可測試的單元。要想知道如何寫出可測試的代碼,就得了解 Mock 和 Stub 。這也就是你在看一些加減乘除單元測試例子以後,仍然不知道怎麼測試本身的代碼的緣由。(每次看到這樣的文章就好氣啊_(:з」∠)_)網絡
可是即便重構了,還有一個問題。你總得寫代碼來執行對其餘代碼進行測試吧?這部分的代碼可能很複雜,也可能變得難以維護。因而測試框架就出現了,幫你減輕作測試的負擔。數據結構
簡單說,它們三者之間的關係是:先重構已有代碼,使其成爲可測試的單元,爲接下去的測試作準備。接着寫出對這些單元進行測試的代碼,驗證結果。爲了使測試代碼易於編寫和維護,藉助測試框架。框架
爲了使代碼可被測試,須要對其進行重構。在這個過程當中會遇到一些問題:ide
其餘類
的方法,怎麼測試?該類的其餘方法
,要怎麼測試?對於前兩個問題,能夠用依賴隔離來解決。《單元測試的藝術》的3.1有個用來理解依賴隔離的例子:
航天飛機在送入太空以前,要先進行測試,不然飛到一半出了問題怎麼辦?
而有一部分測試是確保宇航員已經準備好進入太空。可是你又不能讓宇航員真的坐實際的航天飛機去測試。有個辦法就是建造一套仿真系統,模擬航天飛機控制檯的環境。輸入某種特定的外部環境,而後看宇航員是否執行了正確的操做。
在這個例子中,經過模擬外部環境來解除了對實際外部環境(航天飛機進入太空)的依賴。一樣的思路能夠用到測試中。函數
先從寫出可以測試的代碼開始提及吧。
參考文章:Android單元測試 - 如何開始?
這裏的依賴指的是:當前 類A 須要調用 類B 的方法,則說 A 依賴於 B 。
隔離方法:
A 和 B 隔離先後對比:
隔離前:A -> B
隔離後:A -> C -> B
在項目實際代碼以及測試代碼中使用不一樣的B:
這樣作的好處是,一旦隔離完成,之後就沒必要大幅度修改A。在隔離的時候,要將全部依賴項改成從外部傳入。這就須要給類A添加一個set方法,傳入接口C的實現(implement),即上面的D和E。
類A:
public class Calculater { public double divide(int a, int b) { // 檢測被除數是否爲0 if (MathUtils.checkZero(b)) { throw new RuntimeException("divisor must not be zero"); } return (double) a / b; } }
它調用了類B(MathUtils)的 checkZero 方法。因而咱們說類A依賴於類B的 checkZero 方法。須要注意的是這個 MathUtils 不是從外部傳入的 。
類B是一個具體實現的類:
public class MathUtils { public static boolean checkZero(int num) { return num == 0; } }
在知道產生依賴以後,要將類B改爲一個接口(方法名前綴I表示這是一個接口Interface):
public interface IMathUtils { public boolean checkZero(int num); }
在類A的代碼中,將B替換成該接口:
public class Calculater { private IMathUtils mMathUtils = new MathUtils(); // 這裏的代碼改動了 // 這裏添加了set方法。向該類傳入了mathUtils public void setMathUtils(IMathUtils mathUtils){ mMathUtils = mathUtils; } public double divide(int a, int b) { if (mMathUtils.checkZero(b)) { // 這裏的代碼改動了,將靜態類改爲對象 throw new RuntimeException("divisor must not be zero"); } return (double) a / b; } }
以前的B是一個靜態類,不須要聲明,但改爲接口後須要聲明。
接口的實現:
對於實際運行的代碼,須要一個類去實現 IMathUtils 接口,而後傳入 Calculater 。
修改類B:
JAVA public class MathUtils implements IMathUtils{ public boolean checkZero(int num) { return num == 0; } }
對於用於測試的代碼,也須要一個類實現 IMathUtils 接口,而後傳入 Calculater 。但不一樣的是,這個類的實現可能只需添加一個 return 語句,不用細緻實現。
老是正確的接口:
JAVA public class FakeMathUtils implements IMathUtils{ public boolean checkZero(int num) { return true; } }
return的時候,能夠設一個變量,方便配置不一樣取值,不然還得建立新的類。
```JAVA
public class FakeMathUtils implements IMathUtils{
public boolean isZero = true;
public boolean checkZero(int num) { return isZero; }
}
```
若是一個特定的工做單元的最終結果是調用一個第三方對象(而不是返回某個值,即 void ),你就須要進行交互測試。
這個第三方對象不受你的控制。你沒法查看它的源代碼,或者這不是你負責測試的部分。所以你只需確保傳給它的參數是正確的就能夠了。
那麼如何確保傳過去的參數是正確的?
在這以前,要確保已經依賴隔離。
假設接口爲:
public interface IPerson { ... public void doSomethingWithData(String data); }
待測試類的某個方法:
public class A { private String data = ""; ... public void methodA(IPerson person) { ... person.doSomethingWithData(data); } public void setData(String data) { this.data = data; } }
真正使用的 Person 類是如何實現的呢?假設咱們無從得知。咱們的任務是保證傳入的 data 是符合咱們預期的。只要傳入的內容符合預期,那麼就說明咱們要測試的方法是沒問題的。
僞實現:
public class FakePerson implements IPerson { private String data = ""; ... public void doSomethingWithData(String data) { this.data = data; } public String getData() { return data; } }
在調用 methodA 的時候,傳入 FakePerson 實例。
A test = new A(); test.setData("hahahaha"); IPerson fakePerson = new FakePerson(); test.methodA(fakePerson); Assert.AssertEquals("hahahaha", fakePerson.getData());
僞對象 FakePerson 在被 測試類A 的 methodA 方法中調用,該方法會給僞對象傳入某個信息。
僞對象 FakePerson 不對該信息進行進一步處理,只是賦值給類成員變量存儲起來。
因爲僞對象是從外部傳入的 test.methodA(fakePerson);
,所以能夠直接在外部獲取存儲的信息 fakePerson.getData()
。在assert的時候,獲取該信息,查看是否和預期的一致。
參考:
《單元測試的藝術》第四章
在測試以前,要建立一個專門用於測試的類。這個類的類名以Test
結尾。在類裏面添加測試方法,測試方法名前面要加上 test ,接在 test 後面的是被測試的方法名。在該方法內作三件事:
Setup
Action
Verify
測試框架裏的 AssertXxx 是什麼玩意兒?
咱們寫的測試代碼在運行的時候會產生一些結果,驗證這些結果是否符合預期的一個低效方法就是將這些結果輸出到控制檯(Console)或者文件,而後你本身用眼睛一個個去對比。
若是你懶得去比呢?又或者說你對比的時候以爲沒錯,可是其實是由於一個1
和l
的錯誤致使你沒有發現呢?
就讓 Assert 來幫你解決這些煩人的問題吧! Assert (中文爲:斷言)就是讓你將預期的結果和程序運行的結果傳入它的方法裏面,由它來替你作對比的事情。
例如一個測試結果是否相等的 Assert :
assertEquals(你本身算出的結果, 程序運行的結果);
若是兩個結果不一樣,即程序運行的結果不符合你的預期,那麼它就會提示你這裏出現了錯誤。
今後,你就從幾百甚至是幾萬條的測試代碼輸出的對比中解放出來,大大節約了時間。
有些文章標題看着像是介紹單元測試,其實是介紹單元測試框架。測試框架(Junit,Nunit等)其實是提供便於測試的方案的框架,學習這些內容是學習框架的結構,以及如何使用框架定義的各類 assert ,而不是學習單元測試的方法。這二者要區分開來!!!!!
對於剛纔那個接口 IMathUtils ,咱們能夠不用再新建一個類去實現它,而是交給 Mockito 。
IMathUtils mathUtils = mock(IMathUtils.class); // 生成mock對象
when(mathUtils.checkZero(1)).thenReturn(false); // 這裏是快捷實現。它告訴 Mockito :若是在下面代碼調用了mathUtils.checkZero()並傳入參數1
,那麼就讓調用這個方法的地方返回false
這個值。
字符變量
的邊界數值變量
的邊界數值變量
的邊界在單元測試前要重構,在重構前要編寫集成測試。
集成測試 ——> 重構 ——> 單元測試
重構的過程當中,每次只作少許的改動。儘量多的運行集成測試,以此瞭解重構是否使得系統原有的功能被破壞。
要點:關注系統中你須要修復或者添加功能的部分,不要在其餘部分浪費精力。其餘部分等到須要處理的時候再考慮。
先編寫一個單元測試,這個測試針對於這個 BUG 。因爲它是一個 BUG ,因此顯然這個單元測試一開始給出的結果會是失敗的。此時你修復 BUG ,並運行測試。若是測試成功,則表示你成功修復了這個 BUG ;若是測試失敗,則表示 BUG 仍然存在。
換句話說,這個單元測試暴露了這個 BUG 。 BUG 原本沒看出來,而這個單元測試的失敗代表了 BUG 的存在。
添加新功能也是一樣。寫出一個會失敗的測試,表示缺乏這個功能。而後經過修改代碼使得測試經過,就代表你成功添加了新功能。
在本篇的 MathUtils 例子中,經過setMathUtils()傳入 IMathUtils 的實現。這是經過 getter 和 setter 對類的成員變量操做的方法。這種方法稱爲依賴/屬性注入。除此以外,還有其餘方法。
應該對哪些代碼編寫單元測試?哪些代碼不太須要編寫單元測試?
不常常改動的代碼,特別是底層核心的代碼須要編寫單元測試。常常改動的代碼就不太須要編寫單元測試。畢竟你剛寫完單元測試不久,整個代碼就被修改了,你得再從新編寫單元測試。
Mock/Stub
Mock 和 Stub 是兩種測試代碼功能的方法。 Mock 測重於對功能的模擬。 Stub 測重於對功能的測試重現。好比對於 List 接口, Mock 會直接對 List 進行模擬( assert 寫在調用 List 的 test 方法裏面);而 Stub 會新建一個實現了 List 的 TestList ,在其中編寫測試的代碼( assert 寫在這個 TestList 裏面)。《單元測試的藝術》4.2
Stub 不會使測試失敗,僅僅是用來模擬各類場景。 Mock 相似 Stub ,但它還能使用 assert 。
優先選擇 Mock 方式,由於 Mock 方式下,模擬代碼與測試代碼放在一塊兒,易讀性好,並且擴展性、靈活性都比 Stub 好。但須要注意,一個測試有多個 Stub 是可行的,但有多個 Mock 就會產生麻煩,由於多個 Mock 對象說明你同時測試了多個事情。編寫測試代碼時,不對 Stub 進行 assert ,而是統一到最後由 Mock 進行 assert 。若是你對明顯是用作 Stub 的僞對象進行了斷言,這屬於過分指定。《單元測試的藝術》4.5
若是在一個單元測試中,驗證了多個點,你可能沒法知道究竟是哪一個點出了錯。應該儘量分離。
想作單元測試結果作成集成測試
若是既要請求網絡,又要保存數據,還要顯示界面,那就是集成測試了。
在使用斷言確認字符串的時候,應該把整個預期字符串都寫上麼?
在《重構:改善既有代碼的設計》裏面,有個測試讀取文件的例子。
參考:
關於 Android 單元測試的一切 <-在頁面裏搜索該標題