1. 程式人生 > 其它 >Android 單元測試實踐

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);
    }
}
  1. 我們首先宣告一個Calculate型別的變數mCalculate
  2. 我們在setUp中構造一個Calculate物件例項,賦值給mCalculate
  3. 在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 釋出!