Android 單元測試實踐
什麼是單元測試
定義:單元測試就是針對最小的功能單元編寫測試程式碼
Java程式最小的功能單元是方法,因此,對Java程式進行單元測試就是針對單個Java方法的測試。
什麼是JUnit
JUnit是一個開源的Java語言的單元測試框架,專門針對Java設計,使用最廣泛。JUnit是事實上的單元測試的標準框架,任何Java開發者都應當學習並使用JUnit編寫單元測試。
使用JUnit編寫單元測試的好處在於:
- 非常簡單地組織測試程式碼,並隨時執行它們
- JUnit會給出成功的測試和失敗的測試,還可以生成測試報告
- 不僅包含測試的成功率,還可以統計測試的程式碼覆蓋率,即被測試的程式碼本身有多少經過了測試。
- 幾乎所有的IDE工具都集成了JUnit,這樣我們就可以直接在IDE中編寫並執行JUnit測試
對於高質量的程式碼來說,測試覆蓋率應該在80%以上。
單元測試的好處
- 單元測試可以確保單個方法按照正確預期執行。如果修改了某個方法的程式碼,只需確保其對應的單元測試通過,即可認為改動正確。
- 測試程式碼本身就可以作為示例程式碼,用來演示如何呼叫該方法
在編寫單元測試的時候,我們要遵循一定的規範:
- 單元測試程式碼本身必須非常簡單,能一下看明白,決不能再為測試程式碼編寫測試;
- 每個單元測試應當互相獨立,不依賴執行的順序;
- 測試時不但要覆蓋常用測試用例,還要特別注意測試邊界條件,例如輸入為0,null,空字串""等情況。
線上更多暴露的都是異常場景,所以在單元測試中有必要重點驗證相關異常邏輯。
如何編寫單元測試
新增依賴
新建Android專案中app模組的build.gradle中會自動新增如下依賴:
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
- testImplementation :表示Junit單元測試依賴,對應的是test
- androidTestImplementation :表示Android整合測試,對應的是androidTest目錄
在寫單元測試的時候,有些物件在執行時是沒有真實構造的,這個時候我們可以使用mock框架來模擬出一個可用的物件,需要新增如下依賴:
testImplementation 'org.mockito:mockito-core:2.19.0'
新增用例
首先新增一個測試類,這裡我新增一個簡單的計算類:
public class Calculate {
private int mValue;
//+1
public int addOne() {
return ++mValue;
}
//-1
public int reduceOne() {
return --mValue;
}
}
然後在方法名上右鍵滑鼠,如下圖所示,點選"Test":
如果之前該類沒有建立過Test類,則會提示你沒有找到對應的測試類,點選“create Test”即會出現如下彈框:
- Testing Library:測試用例庫,因為我們build.gradle中依賴的是Junit4,所以這裡選擇Junit4即可
- Class name:表示生成的測試檔案型別。一般用預設的即可(業務類後面加上Test作為測試類名)
- Superclass:基類名稱。一般正常填業務類的基類即可
- Destination package:test目錄下生成的Test類的目標包名
- setUp/@Before : 是否生成setUp方法,並且加上@Before註解
- tearDown/@After :是否生成tearDwon方法,並且加上@After註解
- Member:這裡會列出該類提供的所有public方法,這裡你可以選擇對哪些方法新增測試用例
點選ok按鈕,會讓你選擇建立單元測試用例 還是 整合測試用例,如下圖所示:
這裡我們選擇單元測試用例。 然後我們就會在test目錄下找到對應的包名和測試檔案了,如下圖所示:
註解
單元測試的時候用的最多的是上面3個註解:
@Before : 表示該方法在其他所有的Test方法執行之前都會執行一遍。一般用於初始化。
@After :表示每個Test方法執行結束後,都會執行一遍After方法。一般用於回收相關資源
@Test:標識該方法是一個測試方法
新增用例
我們在剛才生成的CalculateTest類中增加如下程式碼:
public class CalculateTest {
private Calculate mCalculate;
@Before
public void setUp() throws Exception {
mCalculate = new Calculate();
}
@After
public void tearDown() throws Exception {
mCalculate = null;
}
@Test
public void addOne() {
Assert.assertTrue(mCalculate.addOne() == 1);
Assert.assertEquals(mCalculate.addOne(), 2);
}
@Test
public void reduceOne() {
Assert.assertTrue(mCalculate.reduceOne() == -1);
}
}
- 我們首先宣告一個Calculate型別的變數mCalculate
- 我們在setUp中構造一個Calculate物件例項,賦值給mCalculate
- 在addOne和reduceOne方法中引用mCalculate,做對應方法的驗證
這裡我們用到了Junit支援的斷言來判斷用例是否通過:
- Assert.assertTrue:支援條件驗證,條件滿足則該用例能通過,否則用例執行會失敗
- Assert.assertEquals:這裡assertEquals過載了多個型別的實現,只是這裡是比較int值而已。
非同步測試
public class CalculateTest {
private Calculate mCalculate;
ExecutorService sSingleExecutorService = Executors.newSingleThreadExecutor();
......
@Test
public void addOneAsync() {
final CountDownLatch signal = new CountDownLatch(1) ;
sSingleExecutorService.execute(new Runnable() {
@Override
public void run() {
Assert.assertTrue(mCalculate.addOne() == 1);
Assert.assertEquals(mCalculate.addOne(), 2);
signal.countDown();
}
});
try {
signal.await();
} catch (InterruptedException e) {
e.printStackTrace() ;
}
}
}
如上程式碼所示,針對非同步場景,我們可以使用到 CountDownLatch 類來針對性的暫停執行執行緒,直到任務執行完成後再喚醒用例執行緒。
注意,上面的try 才是暫停執行執行緒的核心。
Mock測試
有些時候我們不免會引用Android框架的物件,但是我們單元測試又不是執行在真實裝置上的,在執行時是沒有構建出真實的Android物件的,不過我們可以通過mock程式來模擬一個假的物件,並且強制讓該物件的介面返回我們預期的結果。
1.新增mock依賴引用,前面新增依賴項的時候有提到:
testImplementation 'org.mockito:mockito-core:2.19.0'
2.匯入靜態會讓程式碼簡潔很多,這步不是必要的:
import static org.mockito.Mockito.*;
3.建立mock物件
TextView mockView = mock(TextView.class);
4.進行測試插樁
when(mockView.getText()).thenReturn("Junit Test");
下面我們看一個簡單的例子。
首先我們在Calculate 類中新增一個簡單的方法,獲取TextView的文字資訊:
public String getViewString(TextView view) {
return view.getText().toString();
}
然後我們在CalculateTest類中新增測試方法:
@Test
public void mockTest() {
TextView mockView = mock(TextView.class);
when(mockView.getText()).thenReturn("Junit Test");
assertEquals(mCalculate.getViewString(mockView), "Junit Test");
}
最後執行這個用例,正常通過。
引數化測試
當一個方法有引數時,我們可以批量驗證不同引數值,對應的用例是否通過,而不用寫多遍類似的程式碼
1.首先引數化測試,要求我們對測試類新增如下註解
@RunWith(Parameterized.class)
2.定義引數集合
- 方法必須定義為 public static 的
- 必須新增@Parameterized.Parameters
3.定義接收引數和期望引數物件
4.增加對應的用例
我們看下面的例子:
首先我們在Calculate 中新增一個有引數的add方法:
public class Calculate {
private int mValue;
......
public int add(int other) {
mValue += other;
return mValue;
}
}
接著修改測試類
@RunWith(Parameterized.class) //---------@1
public class CalculateTest {
private Calculate mCalculate;
private Integer mInputNumber; //---------@3
private Integer mExpectedNumber;
//---------@4
public CalculateTest(Integer input , Integer output) {
mInputNumber = input;
mExpectedNumber = output;
}
@Parameterized.Parameters //---------@2
public static Collection paramsCollection() {
return Arrays.asList(new Object[][] {
{ 2, 2 },
{ 6, 6 },
{ 19, 19 },
{ 22, 22 },
{ 23, 23 }
});
}
@Before
public void setUp() throws Exception {
mCalculate = new Calculate();
}
@After
public void tearDown() throws Exception {
mCalculate = null;
}
//---------@5
@Test
public void paramsTest() {
assertEquals(mExpectedNumber, Integer.valueOf(mCalculate.add(mInputNumber)));
}
}
@1 : 給類添加註解RunWith(Parameterized.class)
@2 : 新增資料集合方法,用@Parameterized.Parameters 註解修飾
@3 : 新增輸入引數和期望引數
@4 : 新增構造方法,供給輸入引數和期望引數賦值
@5 : 新增測試方法,直接使用輸入引數和期望引數進行驗證
異常測試
異常驗證通過@Test註解引數來指定:
@Test(expected = InvalidParameterException.class)
看下面具體的例子:
public class Calculate {
private int mValue;
public int addException(int other) {
if (other < 0) {
throw new InvalidParameterException();
}
return add(other);
}
}
測試類如下:
@RunWith(Parameterized.class)
public class CalculateTest {
private Calculate mCalculate;
@Test(expected = InvalidParameterException.class)
public void exceptionTest() {
mCalculate.addException(-1);
}
}
這裡可以注意以下幾點:
- expected的異常如果是丟擲異常的基類,用例測試也是可以通過的
- 若沒有新增expected引數,則用例會失敗
執行用例
- 執行單個用例方法
點選左側綠色箭頭,會彈出如上圖選單,單機Run 即可執行該用例。
- 批量執行某個類的所有用例
如上圖所示,選中測試類檔案,右鍵執行 "Run 類名",就會批量執行該類所有的用例了
- 批量執行專案所有用例
如上圖所示,右鍵包名,執行"Run Test in 包名" 即可執行該包下所有類對應的用例
匯出測試報告
在執行完測試用例之後,我們可以匯出測試報告,如下圖所示:
檢視測試覆蓋度
如上圖所示:點選converage按鈕,在右邊視窗會彈出如下覆蓋情況,這裡從3個方面統計測試覆蓋度:
- class
- method
- Line
最後,我們可以匯出覆蓋報告.
本文由部落格一文多發平臺 OpenWrite 釋出!