天天看點

Android - 不完全測試驅動開發實踐 - 初級篇前言導讀介紹JUnit、Mock與PowerMock配置環境開始之前資料倉庫(DataRepository)設計資料倉庫(DataRepository) - 用例編寫與實作好處與缺陷

前言

測試驅動開發(TDD)是我一直想要嘗試和使用開發方法,但是直至今天才有機會第一次将其應用到正式開發階段。

從開始的模糊,到慢慢了解如何使用,再到借助它将邏輯捋的越來越清楚,再到之後每次跑完所有測試帶給我的信心,我知道這就是我想要的,開發過程再也不是碰運氣,我擁有了使用代碼測試代碼的能力。

因為是不完全從測試驅動開發,本片文章有所不準确的地方也請大家指正。

感謝我的團隊~

導讀

在所有開始之前,需要給大家介紹一些簡要的關于TDD的知識,大家可以從如下位址了解到什麼是TDD以及為什麼需要TDD:

  1. 維基百科 - 測試驅動開發
  2. 維基百科 - Test-driven development
  3. 讀《推行TDD的思考》有感
  4. 你今天寫了自動化測試嗎

本片文章以一個假設的需求為切入點 - 資料倉庫設計(DataRepository),從如下角度來踐行TDD:

  1. 介紹JUnit、Mock與PowerMock
  2. 配置環境
  3. 資料倉庫設計思路
  4. 資料倉庫測試開發思路
  5. 帶來的好處與缺陷

該篇文章僅設計邏輯測試部分,并不涉及UI測試,請提前知曉。

介紹JUnit、Mock與PowerMock

在Java的世界中,TDD的基礎是單元測試,而Junit就是一個非常強大的單元測試庫。

當我們初建一個Android項目時,Android Studio就已經幫我們準備了一些專門用于單元測試的目錄,一個空項目如下所示:

UnitTestDemo
    - app
        - src
            - androidTest
            - main
            - test
           

其中,

test

main

兩個目錄是我們這次主要關心的:

  • androidTest

    :目錄專門用于測試UI邏輯
  • main

    目錄專門用于編寫項目源碼
  • test

    目錄用門用于測試業務邏輯代碼

在此處Junit就不詳細介紹了,更具體的可以參看這裡:

https://zh.wikipedia.org/wiki/JUnit

https://junit.org/junit5/

在這個部分,把關注點放在Mock與PowerMock上,之是以這麼說是因為Junit為我們提供了測試代碼的可能性,但是當項目依賴于其他子產品時,我們可以借助Mock來模拟依賴的類,來控制我們的測試流程。

當我們處于Android環境時更是如此,當我們需要依賴

Handler

Broadcast

或者其他與環境相關的代碼時,如果不去Mock它,别說測試了,連運作恐怕都運作不起來。

如果說Junit給了我們汽車,那麼Mock與PowerMock就是給了我們飛機。

上文中經常提到的Mock實際上指的是Mockito架構,在首頁中他是這麼介紹自己的:

Tasty mocking framework for unit tests in Java (美味可口的模拟測試架構 - Java)

Mocktio為我們提供了這樣一種能力:模拟一個類,不真實的執行它,而是模拟執行并傳回我們想要的資料,且可以去驗證類的行為。

下面是它官網的一段執行個體,展示了上面我們說到的能力:模拟、執行、傳回、驗證。

import static org.mockito.Mockito.*;

// mock creation
// 模拟一個清單
List mockedList = mock(List.class);

// using mock object - it does not throw any "unexpected interaction" exception
// 執行某些行為
mockedList.add("one");
mockedList.clear();

// stubbing appears before the actual execution
// 模拟傳回值
when(mockedList.get(0)).thenReturn("first");

// selective, explicit, highly readable verification
// 驗證行為是否被執行到
verify(mockedList).add("one");
verify(mockedList).clear();

// the following prints "first"
// 下面會列印出first,與上面模拟傳回的一緻
System.out.println(mockedList.get(0));           

切換到Android場景中,我們可以做到下面做種樣子,驗證

Handler

post

是否被執行;模拟

Handler

post

方法執行,驗證

Runnable

run

方法是否被調用等。

@Test
public void testHandler() {

    Handler handler = mock(Handler.class);

    // 驗證Handler的post方法是否被執行了
    handler.post(new Runnable() {
        @Override
        public void run() {

        }
    });
    verify(handler).post(any(Runnable.class));

    // 模拟post方法執行,并驗證run方法有沒有被執行
    when(handler.post(any(Runnable.class))).thenAnswer(new Answer() {
        @Override
        public Object answer(InvocationOnMock invocation) throws Throwable {
            Runnable runnable = invocation.getArgument(0);
            runnable.run();
            return null;
        }
    });
    Runnable spy = spy(new Runnable() {
        @Override
        public void run() {

        }
    });
    handler.post(spy);
    // 驗證run方法是否被執行
    verify(spy).run();
}           

雖然Mockito已經很強大了,但是它還是有不能做到的事情,它不能模拟類中的靜态方法、私有方法、final方法,于是就有了衍生架構PowerMockito。

雖然PowerMockito僅有2000不到的Star,但是它确實還挺好用。
// 被測試的類與靜态方法
public class Static {

    public static boolean isPass() {
        return false;
    }
}

// 使用PowerMockito測試靜态方法
@Test
public void testStaticMethod() {
    PowerMockito.mockStatic(Static.class);

    when(Static.isPass()).thenReturn(true);

    assertThat(true, is(Static.isPass()));
}
           

雖然PowerMockito很強大,但是還是不要過多使用,尤其是針對私有方法與final方法。

PowerMockito更多使用方法請檢視這裡與這裡。

配置環境

Mockito與PowerMockito的環境配置也很簡單,在Android Studio工程中加入如下依賴:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    testImplementation 'junit:junit:4.12'
    testImplementation 'org.mockito:mockito-core:2.8.9'
    testImplementation 'org.hamcrest:hamcrest-all:1.3'
    testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
    testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
}           
注意:mockito-core的最近版本是2.18.3,但是請不要随意更新,因為目前powermock還未相容最新版本。

在編寫測試類時,還需要在類頭部加上

@Runwith(PowerMockRunner.class)

@PrepareForTest({xxx.class})

等注解,告訴Junit與PowerMock目前的運作環境與想要模拟的含有靜态方法的類。

更詳細的關于配置

PowerMockito

的文檔請檢視這裡。

開始之前

在開始TDD之前,讓我們再看一下要良好的踐行TDD都需要注意哪些? (可參閱這裡)

Android - 不完全測試驅動開發實踐 - 初級篇前言導讀介紹JUnit、Mock與PowerMock配置環境開始之前資料倉庫(DataRepository)設計資料倉庫(DataRepository) - 用例編寫與實作好處與缺陷

首要需求分析,将需求分解為任務清單,再從清單中挑選一個任務,轉換成一組測試用例,然後不斷循環去實作。

快速的讓測試用例變綠。

重構

識别壞味道,進行重構。

接下來着手學習TDD吧。

資料倉庫(DataRepository)設計

核心的需求點如下:

  1. 能夠多級緩存複用資料(記憶體、檔案、網絡)
  2. 能夠同步、異步擷取資料
Android - 不完全測試驅動開發實踐 - 初級篇前言導讀介紹JUnit、Mock與PowerMock配置環境開始之前資料倉庫(DataRepository)設計資料倉庫(DataRepository) - 用例編寫與實作好處與缺陷

對外API設計:

  1. 同步全量擷取視訊資訊

    List<VideoInfo> getAllVideoSync();

  2. 異步全量擷取視訊資訊

    List<VideoInfo> getAllVideoAsync();

邏輯層中 :

  1. 擷取資料時,需要按照先記憶體、再檔案緩存、最後網絡資料的順序來擷取資料。

在本篇文章中,我們從任務清單中選取同步全量擷取視訊資訊這個任務,并轉化成為一組測試用例:

  1. 擷取SDK執行個體
  2. 調用

    getAllVideoSync()

    方法
  3. 從記憶體中擷取資料
  4. 先從記憶體拿資料,為空再從檔案緩存中擷取資料
  5. 先從記憶體拿資料,為空再從檔案緩存中拿資料,為空再從網絡中擷取資料
  6. 校驗網絡資料是否被存儲到檔案緩存中
  7. 校驗資料是否正确

做完用例分解後,就讓我們一步一步按照“紅 - 綠 - 藍”的節奏來編寫用例與邏輯吧。

資料倉庫(DataRepository) - 用例編寫與實作

在Demo中,每完成一個Case的編寫與實作都會Commit一次,我會盡量做到完善。

讓我們踏上征途吧。

CI - 擷取SDK執行個體

先給我們的SDK起個好聽的名字

DataRepository

,再把它做成一個單例。

再考慮下如何驗證有效性!隻需要斷言判定

getInstance()

擷取的資料不為空就好了。

Commit記錄

CI - 調用

getAllVideoSync()

方法

DataRepository

中建立了

getAllVideoSync()

方法并建立了

VideoInfo

類。

由于本次的目的隻是驗證

getAllVideoSync()

方法是否能夠正确調用,是以我們不關心傳回結果。

Commit記錄

CI - 從記憶體中擷取資料

本次想要從記憶體中擷取緩存資料,是以關心傳回結果。

可以直接在TestCase中直接給

DataRepository

mAllVideoInfo

指派,來達到模拟記憶體緩存值存在的情況。

Commit記錄

CI - 先從記憶體拿資料,為空再從檔案緩存中擷取資料

從這一個Case開始就變得複雜一些了。雖然Commit記錄中詳細的寫明了我都做了什麼,但是還是簡要介紹一下做這些操作的思路。

步驟一:重構了擷取

DataRepository

的方法,原因是每次都寫一下擷取邏輯很麻煩,還不如封裝一下。

步驟二: 由于驗證的行為是記憶體資料為空,但是緩存值存在,那麼就需要修改驗證結果。

// 驗證檔案緩存值存在,擷取結果不為null
List<VideoInfo> userInfoList = instance.getAllVideoSync();
assertThat(userInfoList, is(nullValue()));           

步驟三:運作單測之後自然是紅色錯誤,我們轉而進入

DataRepository

getAllVideoSync

内部去實作擷取緩存的邏輯。 自然而然,我們期望有一個幫助類能夠直接拿到緩存的結果,從那個檔案拿我們并不關心,于是我們有了

FileCacheHelper.getAllVideoCache()

,在編寫邏輯之後,代碼如下:

public List<VideoInfo> getAllVideoSync() {

    // 記憶體緩存存在時直接傳回
    if (mAllVideoInfo != null) {
        return mAllVideoInfo;
    }

    // 擷取檔案緩存,檔案緩存存在時指派給記憶體緩存并傳回資料
    mAllVideoInfo = mFileCacheHelper.getAllVideoCache();
    if (mAllVideoInfo != null) {
        return mAllVideoInfo;
    }
    return mAllVideoInfo;
}           

此時是連編譯都無法通過的,原因是還沒有

mFileCacheHelper

這個變量,而且連

FileCacheHelper

類也沒有建立,此外,

FileCacheHelper

也不能直接在

UserInfoManager

中建構出來,如果要建構,那麼注意力就轉移到

FileCacheHelper

的實作上了。

當建立完

FileCacheHelper

以及

getAllVideoCache

方法,再回到測試用例中。 由于本階段并不關心

FileCacheHelper

類的真是邏輯如何,是以

mock

來模拟一個

FileCacheHelper

的執行個體對象,并且希望它的

getAllVideoCache()

方法預設傳回

null

// 模拟一個FileCacheHelper執行個體
@Mock
FileCacheHelper mFileCacheHelper;


// Refactor Get Instance Method
private DataRepository getNewInstance() {
    ...
    // 讓FileCacheHelper.getAllUserInfoCache預設傳回null
    when(mFileCacheHelper.getAllVideoCache()).thenReturn(null);

    return instance;
}           

再回到

getAllVideoSync_memoryCacheNull_diskCacheExist

單元測試中,由于是驗證檔案緩存存在的情況,是以期望

mFileCacheHelper.getAllVideoCache()

傳回一個有效值:

// 假設邏輯調用mFileCacheHelper.getAllVideoCache()時,傳回一個空清單
when(mFileCacheHelper.getAllVideoCache()).thenReturn(new ArrayList<UserInfo>());           

完成這一步,單元測試就能跑通了,而

getAllVideoSync()

中關于檔案緩存擷取的邏輯也完成了。

Commit記錄 - 重構擷取

DataRepository

的方法

Commit記錄 - 實作檔案緩存邏輯

CI - 先從記憶體拿資料,為空再從檔案緩存中拿資料,為空再從網絡中擷取資料

這裡的步驟和上面一個非常類似,是以就不重複表述了。

從這兩個Case中可以知道,完全可以在僅關心

DataRepository

的情況下,把邏輯補充完整,暫時不關心的

FileCacheHelper

NetHelper

可以暫放一邊,通過模拟它們來跑通邏輯。

相信不用我說,你也一定知道這多麼有用處。

Commit記錄

CI - 校驗網絡資料是否被存儲到檔案緩存中

為了確定模拟的

FileCacheHelper

類中的方法被調用,可以使用如下的方法:

// 驗證行為,儲存到緩存中是否被執行了
 verify(mFileCacheHelper).saveDataToCache(list);           

Commit記錄

好處與缺陷

如果仔細看完上面資料倉庫的例子,相信你對測試驅動開發一定有一些認識了,下面就談談它的好處與難點。

好處:

  1. 給予開發者信心,讓你知道自己寫的代碼是可靠的
  2. 提升對項目需求分析、分解任務、安排優先級的能力
  3. 提高重構的能力
  4. 将焦點聚集在目前關注的地方

難點:

1. 思維方式的轉變 - “很多人不懂“意圖式程式設計”,總是習慣先實作一個東西,再去調用它。而測試先行就要求先使用,再實作。這樣能少走很多彎路,減少返工。”

2. 測試架構的選型與使用,雖然Mockito與PowerMockito已經很簡單了,但是還是有一些學習成本的。

最後引用一段話,也是我想說的:

最後我想說:

TDD不是銀彈,不可能适合所有的場景,但這不應該成為我們拒絕它的理由。

也不要輕易否定TDD,如果要否定,起碼要在認真實踐過之後。

最後,祝好~