2016年11月29日 星期二

Testing: 第一個單元測試

前言

前一篇Testing: Android環境的單元測試中,我們利用Android Studio的環境,加上用簡單預設的測試案例,介紹了該環境中如何運行單元測試。接下來則要正式寫一個測試案例,並且說明需要認識些什麼?

FileParser程式

由於我不討論測試驅動開發,所以不以該方式來建立我們的程式碼,直接先建立一個名為FileParser的類別,而這個類別有一個isValidLogFileName()的方法,主要是將傳進來的檔名轉成小寫後,檢查檔案名稱是否以”.exe"做結尾,是的話就回傳”true”,反之返回”false”,程式碼如下。

public class FileParser {

    public boolean isValidLogFileName(String fileName) {
        if (fileName == null || fileName.length() == 0) {
            throw new IllegalArgumentException("請提供檔名!");
        }

        return fileName.toLowerCase().endsWith(".exe");
    }
}

運行第一個測試案例

接著,我們建立一個名為FileParserTest1的測試類別,並且新增一個名為testFileNameWithCorrectSuffixInUppercaseIsConsideredValid()的測試案例,用來檢驗輸入一個檔名,是否傳回預期的布林值,建立過程如下圖,程式碼如下。




    @Test
    public void testFileNameWithCorrectSuffixInUppercaseIsConsideredValid() throws Exception {
        // Arrange
        FileParser fileParser = new FileParser();
        String fileName = "Whatever.EXE";
        boolean expectedResult = true;

        // Act
        boolean actualResult = fileParser.isValidLogFileName(fileName);

        // Assert
        assertEquals(expectedResult, actualResult);
    }

從這段程式碼和建立過程中,可以知道撰寫一個測試案例很簡單,透過Android Studio就能為我們產生所需要的案例,並且只需要給予這個測試案例一個名稱,當然命名能表現這個測試的意圖是最好的,雖然名稱很長,卻很容易知道這個測試是為了檢驗一個存在大小寫的檔案名稱是否真能正確的轉為小寫,並且以.exe為結尾。至於該不該在每個測試案例中以test為開頭,其實不用,因為已經很明顯就是一個測試方法了,只是個人習慣以test為開頭而已。

再來,該如何撰寫一個測試案例的內容?在XUnit的測試模式(Pattern)中,有個3A原則,也就是一個單元測試主要包含三個行為。

  • 準備(Arrange): 產生物件,並且運行必要的設置(例如: Mock、Stub、Spy物件或物件的初始化,若是靜態方法則毋需初始。)。 從上述程式碼也能看見,Arrange就是在進行測試物件的產生、準備傳進去的檔案名稱,以及預期的結果應該為”true”,如下圖。


  • 操作(Act): 針對你的測試物標,進行測試的行為,簡言之,就是針對你的測試目標進行呼叫和使用。Act也就是呼叫了isValidLogFileName方法,並且將檔案名稱作為傳入參數,如下圖。


  • 斷言(Assert): 最後你預期測試目標的某個行為是在你預期的。透過assertEquals的斷言方法,斷言實際結果與預期結果是相等的,如下圖。


最後,來執行看看是否順利通過測試?如下圖所示,我們很順利的通過第一個測試了。


完成其他測試案例

順利完成第一個測試案例,但對目標物件的測試還沒有結束,前一個案例是測試傳入的字串有大小寫字元的夾雜,若將全部轉為小寫,是否也能正常運作?我們繼續完成這個案例,程式碼如下所示。

    @Test
    public void testFileNameWithCorrectSuffixInLowercaseIsConsideredValid() throws Exception {
        // Arrange
        FileParser fileParser = new FileParser();
        String fileName = "whatever.exe";

        // Act
        boolean actualResult = fileParser.isValidLogFileName(fileName);

        // Assert
        assertTrue(actualResult);
    }

這個測試案例與前一個差不多,差別就在我傳入的檔案名稱改成全部小寫,以及斷言的方法改成assertTrue,這個方法只要帶入一個實際的boolean值就可以了。執行後,也是順利通過測試。

測試例外

至目前為止,都是測試一般的狀況,但有時會面臨到例外的情況,像FileParser程式裡面就包含了一個例外,也就是在檔案名稱為空的情形下會拋出IllegalArgumentException,這時又該如何進行測試?同樣地,如同上面的流程建立一個測試檔案名稱為空的案例,並且執行它,程式碼與執行結果如下。

    @Test
    public void testEmptyFileNameResultsInExceptionBeingThrown() throws Exception {
        // Arrange
        FileParser fileParser = new FileParser();
        String fileName = "";

        // Act
        boolean actualResult = fileParser.isValidLogFileName(fileName);

        // Assert
        assertFalse(actualResult);
    }


可以看到測試失敗了,而且不管你怎樣斷言結果,它始終是失敗的,因為這邊已經拋出例外,程式就是無法正常運行,該怎麼斷言這個結果?從這個結果看來,遇到檔案名稱為空的情形下,就預期測試會拋出IllegalArgumentException例外,並且錯誤訊息會顯示”請提供檔名!”。可以重新改寫測試案例,程式碼與執行結果如下。

    @Test
    public void testEmptyFileNameResultsInExceptionBeingThrown() throws Exception {
        // Arrange
        FileParser fileParser = new FileParser();
        String fileName = "";
        String expectedErrorMessage = "請提供檔名!";

        try {
            // Act
            fileParser.isValidLogFileName(fileName);
            fail("這個測試預期拋出IllegalArgumentException例外!");
        } catch (IllegalArgumentException expected) {
            // Assert
            assertEquals(expectedErrorMessage, expected.getMessage());
        }
    }


順利通過測試了,可以觀察一下,我使用try-catch來捕捉例外,並且從catch區域來進行斷言,而在try區域刻意呼叫一個fail方法,確保測試只要順利通過try區域就視為失敗。不過,測試例外還可以使用另一種簡潔的方法,可以透過@Test這個Annotation來斷言拋出的例外,程式碼與執行結果如下。

    @Test (expected = IllegalArgumentException.class)
    public void testExceptionForEmptyFileNameMakesSense() throws Exception {
        FileParser fileParser = new FileParser();
        String fileName = "";
        fileParser.isValidLogFileName(fileName);
    }


測試的程式碼是不更簡潔、乾淨了?無論使用哪種,都是可以的。

Setup與Teardown

完成測試案例後,可以觀察前幾個測試案例,每個都在Arrange的階段中初始FileParser物件,重複對我們來說是很困擾的事情,為了改進這個方式,可以利用Setup方法來為我們在每個案例執行前進行該物件的初始化,這樣就不用在每個測試案例中撰寫重複的程式碼。新增一個FileParserTest2的測試類別,透過右鍵產生一個Setup方法,如下圖。



可以看到setup方法內做的事情很簡單,就是初始化該物件而已,其相對的Annotation為@Before。然而我們有時會需要在每個測試結束時,釋放掉某些資源,這時就可以利用Teardown這個方法,其相對的Annotation為@After,產生方式如同Setup,程式碼如下。

    @After
    public void tearDown() throws Exception {
        // 不建議這樣做,僅為示範。
        this.fileParser = null;
    }

Teardown裡面的事情僅為示範,實際上也不需要這樣釋放這個資源。所以,實際採用Setup和Teardown的測試案例如下。

public class FileParserTest2 {
    private FileParser fileParser;

    @Before
    public void setUp() throws Exception {
        this.fileParser = new FileParser();
    }

    @Test
    public void testFileNameWithCorrectSuffixInUppercaseIsConsideredValid() throws Exception {
        // Arrange
        String fileName = "Whatever.EXE";
        boolean expectedResult = true;

        // Act
        boolean actualResult = this.fileParser.isValidLogFileName(fileName);

        // Assert
        assertEquals(expectedResult, actualResult);
    }

    @Test
    public void testFileNameWithCorrectSuffixInLowercaseIsConsideredValid() throws Exception {
        // Arrange
        String fileName = "whatever.exe";

        // Act
        boolean actualResult = this.fileParser.isValidLogFileName(fileName);

        // Assert
        assertTrue(actualResult);
    }

    @Test
    public void testEmptyFileNameResultsInExceptionBeingThrown() throws Exception {
        // Arrange
        String fileName = "";
        String expectedErrorMessage = "請提供檔名!";

        try {
            // Act
            this.fileParser.isValidLogFileName(fileName);
            fail("這個測試預期拋出IllegalArgumentException例外!");
        } catch (IllegalArgumentException expected) {
            // Assert
            assertEquals(expectedErrorMessage, expected.getMessage());
        }
    }

    @Test (expected = IllegalArgumentException.class)
    public void testExceptionForEmptyFileNameMakesSense() throws Exception {
        String fileName = "";
        this.fileParser.isValidLogFileName(fileName);
    }

    @After
    public void tearDown() throws Exception {
        // 不建議這樣做,僅為示範。
        this.fileParser = null;
    }
}

Beforeclass與Afterclass

介紹完Setup與Teardown後,我們來看看Beforeclass(對應的Annotation為@BeforeClass)與Afterclass(對應的Annotation為@AfterClass)這兩個方法。有些物件有時可能只需要在一開始初始與釋放,不需要每次執行時都做這些事情,這時就可以使用Beforeclass與Afterclass,做個實驗來觀察其差異,程式碼與執行結果如下。

public class BeforeClassAndAfterClassTest {

    @BeforeClass
    public static void beforeClass() throws Exception {
        System.out.println("beforeClass_called");
        System.out.println();
    }

    @Before
    public void setUp() throws Exception {
        System.out.println("setUp_called");
        System.out.println();
    }

    @Test
    public void testcase1() throws Exception {
        System.out.println("testcase1_called");
        System.out.println();
    }

    @Test
    public void testcase2() throws Exception {
        System.out.println("testcase2_called");
        System.out.println();
    }

    @After
    public void tearDown() throws Exception {
        System.out.println("tearDown_called");
        System.out.println();
    }

    @AfterClass
    public static void afterClass() throws Exception {
        System.out.println("afterClass_called");
        System.out.println();
    }
}

執行結果:
beforeClass_called

setUp_called

testcase1_called

tearDown_called

setUp_called

testcase2_called

tearDown_called

afterClass_called

有看到setup與teardown是在每個測試案例執行前後都被呼叫了?而beforeclass與afterclass則是最初與最後才被呼叫,這就是之間的差異。

忽略測試

有時測試程式發生問題,但是你又暫時不想去更動,這時你可以先忽略該測試案例,只讓其他測試案例執行,這時我們可以使用@Ignore這個Annoation,我們可以看看差異,程式碼與執行結果如下。

不使用@Ignore: 

public class IgnoreTest {

    @Test
    public void testcase1() throws Exception {
        System.out.println("testcase1_called");
        System.out.println();
    }

    @Test
    public void testcase2() throws Exception {
        System.out.println("testcase2_called");
        System.out.println();
    }

    @Test
    public void testcase3() throws Exception {
        System.out.println("testcase3_called");
        System.out.println();
    }

    @Test
    public void testcase4() throws Exception {
        System.out.println("testcase4_called");
        System.out.println();
    }

    @Test
    public void testcase5() throws Exception {
        System.out.println("testcase5_called");
        System.out.println();
    }
}

不使用@Ignore的執行結果:

testcase1_called

testcase2_called

testcase3_called

testcase4_called

testcase5_called

忽略case3與case5:

public class IgnoreTest {

    @Test
    public void testcase1() throws Exception {
        System.out.println("testcase1_called");
        System.out.println();
    }

    @Test
    public void testcase2() throws Exception {
        System.out.println("testcase2_called");
        System.out.println();
    }

    @Test
    @Ignore
    public void testcase3() throws Exception {
        System.out.println("testcase3_called");
        System.out.println();
    }

    @Test
    public void testcase4() throws Exception {
        System.out.println("testcase4_called");
        System.out.println();
    }

    @Test
    @Ignore
    public void testcase5() throws Exception {
        System.out.println("testcase5_called");
        System.out.println();
    }
}

使用@Ignore的執行結果:

testcase1_called

testcase2_called

testcase4_called

使用的前後可以看出,測試案例3與測試案例5都被忽略而沒有執行,不過忽略的情形很少,也不建議使用這種方式,因為忽略測試程式碼很容易讓人混淆,會不清楚那些程式碼為何存在卻又不刪除。

組合測試

在某些情況,我們的測試類別可能會很多,但有時想執行部分的測試類別(例如: 當你只重構A與B類別,但是其他類別完全沒有更動過。),這時我們可以使用測試套件(Test Suite)組合你想要執行的測試類別,程式碼與執行結果如下。

@RunWith(Suite.class)
@Suite.SuiteClasses({
        FileParserTest1.class,
        FileParserTest2.class,
        IgnoreTest.class
})
public class Sample1TestSuite {
}


看到了?我們只執行了部分的測試類別,其餘未加入測試套件的都沒有被執行。@RunWith這個Annotation表示這個類別是以Suite來執行,@Suite.SuiteClasses()裡面則是以陣列的方式存放你要測試的類別名稱,以上述為例,這個組合測試就只測FileParserTest1、FileParserTest2與IgnoreTest這三個而已。

小結

至目前為止,我們已經知道該如何撰寫一個測試案例、3A原則、測試例外、Setup, Teardown, Beforeclass與Afterclass、忽略測試與組合測試這幾個議題,接下的內容會介紹重要的單元測試內容,讓我們慢慢來練習囉!

Source Code

Github: https://github.com/xavier0507/UnitTestSample.git

2016年11月28日 星期一

Testing: Android環境的單元測試

前言

在前一篇Testing: 單元測試簡介一文中介紹完單元測試後,也認識了不依賴單元測試框架,而改用手動撰寫單元測試是非常耗時和麻煩的,好不容易讓已寫好的程式碼運行穩定,當下次撰寫新的程式碼時,那些擾人的手動判斷又要重複一次,真是太可怕了。完全手工的方式撰寫,機械化地執行它們,很容易出錯也浪費時間,我們希望能透過自動化建置來解決這些問題,這時我們就需要單元測試框架的輔助。

第一個單元測試

在Android的開發中,撰寫的語言是以Java為基礎,所以測試的框架便是JUnit。當你開啟一個新的Android專案時,你無須引入JUnit任何的函式庫,Gradle當中就已經幫你包含進去了,如下所示。

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

    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })

    compile 'com.android.support:appcompat-v7:25.0.1'

    testCompile 'junit:junit:4.12'
}

Android Studio同時也會為你建置一個單元測試案例(Test case),可以先看看這個預設的測試案例具備什麼元素,如下所示。

public class ExampleUnitTest {

    @Test
    public void addition_isCorrect() throws Exception {
        assertEquals(4, 2 + 2);
    }
}

這個測試案例的類別名稱為ExampleUnitTest,並且隸屬於test package底下,而這個package都是放置與單元測試相關的案例,也就是直接運行在JVM層的測試。這個類別包含了一個名為@Test的annotation,測試案例稱為addition_isCorrect(),裡面寫了assertEquals(4, 2 + 2)的程式碼,所以可以知道撰寫一個測試案例至少要具備這些元素。

  • 測試類別: 這個類別主要包含你應有的測試案例。
  • Annotation: 若沒有增加這個@Test這個Annotation,編譯時就會無法得知這個方法是否為測試方法。你也可以試著將這個移除,就會發現原本左邊的測試按鈕會消失,變為一般的方法了,如下圖。

  • Assertion: Assertion的意思為斷言,其實就是斷定你所預期的行為是否正常。

測試結果

當知道具備這些基本元素後,可以試著執行看看,並觀察其結果。在檔案列表中,對著該測試類別點選右鍵,選擇Run ‘XXXXXX’,如下圖。


或是程式碼中針對某個測試案例執行也行,點選方法左邊的執行圖示即可,如下圖。


執行完後的結果如下圖所示,綠燈是通過;紅燈則是失敗。



小結

順利執行完簡單的Android單元測試,對於基本的實作有了初步概念,一個測試案例至少需具備測試類別名稱、Test的Annotation,以及你所斷言的預期行為,下一篇我們要正式進入練習單元測試的領域啦!

2016年11月25日 星期五

Testing: 單元測試簡介

前言

先前有先寫一篇如何利用Android Studio建立Unit/Instrument測試與報告,主要是關於Unit/Instrument在Android Studio中的環境建立、執行與報告的產生,但完全沒有提到Android單元測試(Unit Test)的內容,所以想寫一些相關的議題,一方面作為筆記,另一方面也提供想實作不得其門而入的朋友們,透過循序漸進的方式了解。

什麼是單元測試

在軟體開發的領域中,它並不是新奇的概念與名詞,早在SmallTalk語言的開始,單元測試就已經出現,而且一次次地被驗證能成為開發人員提升軟體品質和深入理解系統功能需求的最佳方法之一。

那什麼是單元測試?一個單元測試就是一段程式碼,通常這段程式碼會是一個方法(或稱函式),這段程式碼也會呼叫另一段程式碼,然而為了要去檢驗它,你會做些假設並且斷定它的正確性。如果假設是錯誤的,那單元測試就失敗了。從這個描述中,我們能得知一個單元測試會有個測試的目標或對象,這個目標我們則稱為SUT(System Under Test)或是CUT(Class Under Test / Code Under Test)。從呼叫系統的一個公開方法(public method)到所預期的最終結果,這段系統產生的行為狀態,我們就能稱為一個單元(Unit)。再更白話點,物件導向的領域中,一個單元就是一個類別內的方法,然後你寫了一個測試程式去斷定這個方法的正確性或是能否正常工作,那這就是單元測試。

單元測試的特性

但是,上面描述的只是基本的概念,更明確的單元測試則是包含以下特性。
  • 單元測試必須是自動化,可重複執行的。
  • 如果單元測試失敗了,我們應該很容易發現預期的結果,並且得知問題所在。
  • 單元測試要容易實現的。
  • 它應該要獨立於其他測試的運行。
  • 無論執行多少次,都是穩定的結果。
  • 測試程式要完全控制測試的單元。
  • 任何人都能一鍵執行它。
  • 單元測試的速度是很快的。
你可以試著反問自己,如果你的單元測試很難達到上述條件,或是任何的測試,執行速度不快,結果不穩定,或者要用到被測單元的一個或多個真實環境或依賴物件,那你做的有可能就是整合測試了(例如:你測試時真的需要一個資料庫內的物件或是真實的檔案物件。)。

所謂的整合測試也是對一個工作單元進行測試,但這個測試會使用一到多個真實的依賴物(例如:網路、時間、資料庫、隨機數字...等等)。若覺得上述特性很難記憶,另一種簡單的方式叫做F.I.R.S.T準則。
  • Fast:快速。
  • Independent:獨立。
  • Repeatable:可重複。
  • Self-Validating:可反應驗證結果。
  • Timely:及時。

測試目標

對於單元測試的結果斷定,會涉及下列三種形式。
  • 預期被測物件的回傳值:被呼叫的公開方法會回傳一個值。
  • 預期被測物件的的狀態:在方法被呼叫的前後,被測物件的狀態或行為有一定的變化,這種變化無需查詢私有狀態就可判斷。(例如:一個登入物件,在一個會員登入後,其相對應的狀態應由未登入轉為已登入。)
  • 預期被測物件與相依物件的互動:當呼叫了一個不受測試控制的外部系統或物件,其不返回任何值,或返回值都被忽略。(例如:呼叫一個void方法,然而這個方法使用了一個物件,你預期這個方法呼叫後,該物件的某個方法理應被呼叫。)

簡單的單元測試案例

或許你聽過XUnit系列的測試框架(Framework),這系列都是以它們為主的語言開頭字母加上Unit作為名字,像是Java的單元測試框架就是JUnit、C++的單元測試框架則是CppUnit、.Net的單元測試框架稱為NUnit。先不論我們是否使用XUnit系列的框架,平常我們也會為程式撰寫單元測試,因為程式開發者在將軟體產品遞交給測試工程師驗證時,也都會執行基本的測試,這些也是單元測試的一種,例如:我們有可能會撰寫類似下面的測試,來觀察程式是否如預期。

public class SimpleParser {

    public int sum(int a, int b) {
        return a + b;
    }

    public static void main(String[] args) {
        SimpleParser simpleParser = new SimpleParser();

        int expectedResult = 14;
        int actualResult = simpleParser.sum(5, 9);

        if (expectedResult == actualResult) {
            System.out.println("Success!");
        } else {
            System.out.println("Fail");
        }
    }

}

這段程式只是做簡單的加法,會預期 5 + 9 = 14,如果實際結果(actualResult)如同預期結果(expectedResult),那就顯示成功,這也是不依賴測試框架的單元測試。

撰寫單元測試的時機

然而,上述方式寫起來很麻煩,因此我們會使用單元測試框架來將上面的測試案例撰寫更為容易與便利,這也是在下個章節會提到的。稍微理解單元測試後,接下來就是關於何時該寫單元測試?過去,開發者可能會在軟體完成時才進行單元測試,但也越來越多人願意在程式撰寫前就先寫單元測試,我們則稱為「測試驅動開發」(Test-Driven Development, TDD)。測試驅動開發的過程,會先以撰寫失敗的測試案例為主,接著撰寫產品程式碼並測試,再重構或來改進設計,不斷重複這個過程,步驟與細節流程如下。





當然,撰寫單元測試是我們關注的議題,至於是否要導入測試驅動開發,則視公司和團隊性質去討論的,這邊就不多著墨。但是可以確定的是,至少當你在程式、需求有異動,或是已經出現非預期執行結果時,你必須要改變設計或建立重構時,就是該寫單元測試的時機。

小結

以上提到了單元測試、特性與時機,也知道單元測試與整合測試的區別,接下來我們就該多練習撰寫單元測試,習慣單元測試,讓你在早期發現軟體的缺陷,提升軟體的品質。雖然單元測試無法保證你的軟體完全不會有問題,卻能反映在你有測試到的部分都會是沒有問題且穩定的,而且其他開發者也能從你的測試案例中,得知系統的需求,快快加入單元測試的世界吧!

2016年11月2日 星期三

Android: 利用Google Drive API撰寫apk檔案上傳器

前言

在Android的開發上,每當建置完apk後,在版本控制或是將apk釋出給測試人員或專案管理者測試時,有時相關測試人員會來向開發者尋求apk檔,倘若該情形是很頻繁的釋出,那這件事情就會變得很擾人。

我們可以透過Jenkins或其他持續整合(Continuous Integration, CI)環境來將我們的apk釋出至DropBox環境或其他空間,當然,若你沒有建立CI,也不想每次建置完apk後,又要手動複製檔案至某個儲存空間,或許可以利用Google Drive API來將你的檔案自動上傳,這篇文章便是教你如何開發一個apk檔案上傳器。

Java實作

Android: 如何在Gradle建立Build Variants及更換APK檔名這篇文章中,我們在使用"gradle assembleDebug/assembleRelease"的指令時,會同時建置出很多apk,所以我會利用這個專案來解釋實作。
  • 申請憑證
    • 由於要在程式內使用Google Drive API,我們需要至Google Developer Console申請一個應用程式內用來識別的憑證。
      • 先在Google Developer Console建立新專案,這裡我命名為GDUSample


      • 點選憑證

      • 選擇OAuth用戶端ID


      • 選擇其他,並輸入一個名稱,這邊我一樣命名為GDUSample

      • 建立後,就能看見憑證的資料,這時選擇下載Json

      • 至資訊主頁點選啟用API,並搜尋Google Drive API



  • 撰寫程式碼
    • 待憑證和API啟用後,我們則要使用Google Drive API進行開發。由於這個檔案上傳器是利用Java程式來開發,就使用你習慣的IDE即可,這裡我是使用Eclipse,並開啟一個新的JAVA專案。

    • 匯入Drive API

    • 選擇Drive API v2,由於我已經匯入,所以下圖會顯示"installed"

    • 匯入下列lib

    • 將先前下載憑證的Json,命名為"client_secret.json",並放置於專案下。



    • 初始HttpTransport與FileDataStoreFactory物件
    public static final String APPLICATION_NAME = "Drive API Java Quickstart";
    public static final java.io.File DATA_STORE_DIR = new java.io.File(System.getProperty("user.home"), getJarPath());
    public static FileDataStoreFactory DATA_STORE_FACTORY;
    public static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
    public static HttpTransport HTTP_TRANSPORT;
    public static final List<String> SCOPES = Arrays.asList(DriveScopes.DRIVE);

static {
        try {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
            DATA_STORE_FACTORY = new FileDataStoreFactory(DATA_STORE_DIR);
        } catch (Throwable t) {
            t.printStackTrace();
            System.exit(1);
        }
}

    • 取得Credential物件
      • 在程式內使用Google Drive API時,我們需要先利用先前下載的憑證,藉由FileInputStream來產生一個GoogleClientSecrets,並且藉由這個物件,取得Credential物件,程式碼片段下。
public static Credential authorize() throws IOException {
        System.out.println("path: " + DATA_STORE_DIR.getPath());

        FileInputStream fileInputStream = new FileInputStream(getJarPath() + "/client_secret.json");
        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY,
                new InputStreamReader(fileInputStream));

        GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY,
                clientSecrets, SCOPES).setDataStoreFactory(DATA_STORE_FACTORY).setAccessType("offline").build();

        Credential credential = new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
        System.out.println("Credentials saved to " + DATA_STORE_DIR.getAbsolutePath());

        return credential;
}

    • 利用Credential取得Drive物件
public static Drive getDriveService() throws IOException {
        Credential credential = authorize();
        return new Drive.Builder(HTTP_TRANSPORT, JSON_FACTORY, credential).setApplicationName(APPLICATION_NAME).build();
}

    • 利用Drive物件的Insert方法,在Google Drive上新增資料夾與檔案
      • 定義欲在Google Drive新增資料夾或檔案的ID。例如我想在名為GoogleDriveUploader這個資料夾中,新增我的apk資料夾和檔案,所以我需要先取得該資料夾的ID,並將該ID定義於程式當中,如下圖所示。


public static final String apksLocationId = "0B-0iN5m8YqglMHJxdy13Z0JkU3c";

      • 使用Insert方法新增資料夾。
        • 這邊資料夾的命名方式是以當天的年月日命名,所以會使用到Date物件。
        • Insert方法的使用很簡單,只要先產生一個Google Drive的File物件後,接著設定Title與MimeType即可。
        • parentId則是上述定義的Id
public File createRemoteApkFolder(String parentId) throws IOException {
        File fileMetadata = new File();
        StringBuffer folderName = new StringBuffer();
        String fileTitle = folderName.append("gduSampleFolder").append(this.util.getDateTime()).toString();

        fileMetadata.setTitle(fileTitle);
        fileMetadata.setMimeType("application/vnd.google-apps.folder");

        if (parentId != null && parentId.length() > 0) {
            fileMetadata.setParents(Arrays.asList(new ParentReference().setId(parentId)));
        }

        File remoteFileFolder = this.service.files().insert(fileMetadata).setFields("id").execute();

        return remoteFileFolder;
}

        • 使用Insert方法新增檔案
          • 新增檔案方式與資料夾無異,只是我們需要指定欲上傳檔案的路徑(filePath)。
          • 新增檔案的MimeType,由於我是將所有apk打包成zip檔,所以我將MimeType指定為"application/octet-stream"。
public File createRemoteApkFiles(String fileTitle, String parentId, String filePath, String mediaContentMimeType)
            throws IOException {
        File fileMetadata = new File();
        fileMetadata.setTitle(fileTitle);
        fileMetadata.setMimeType(mediaContentMimeType);

        if (parentId != null && parentId.length() > 0) {
            fileMetadata.setParents(Arrays.asList(new ParentReference().setId(parentId)));
        }

        java.io.File localFile = new java.io.File(filePath);
        FileContent mediaContent = new FileContent(mediaContentMimeType, localFile);
        File remoteFile = this.service.files().insert(fileMetadata, mediaContent).setFields("id").execute();

        return remoteFile;
}

        • 將程式打包成Runnable Jar File





        • 產生出來的Runnable Jar File需要和你下載的“client_secret.json”放在同一個目錄下,基本上就可以執行。可以在Terminal使用"java -jar GoogleDriveUploader.jar"執行,如下圖所示。

        • 第一次會需要使用者驗證,所以當你執行該檔案,則會跳出要求使用者允許的頁面,按下允許就可以,之後也無需驗證。

撰寫簡單的Shell Script

其實撰寫完Java程式已經算完成,但我們可以撰寫一個Shell Script,讓我們每次執行完"gradle assebmleDebug/assembleRelease"指令後,將所有apk打包成zip,並上傳至Google Drive,甚至可以再利用CI環境整合,當上傳完成後,則發送信件通知相關測試人員。CI部分若有時間,我則會在之後的文章加入。

  • 將你的Jar File與client_secret.json置於專案下,並於Android專案下新增File,且使用.sh副檔名,如下圖所示。

  • 撰寫Shell Script,程式碼如下。
    • $(pwd): 指的是檔案當下所處的位置
    • gradle clean assembleDebug: 則是執行建置Debug環境的apk指令
    • zip -r "路徑/檔名" "路徑/欲打包的檔案": zip則是執行將檔案打包成你所要zip檔
    • java -jar $(pwd)/GDUSample.jar: 執行該路徑下的Jar File
#!/bin/sh
echo Current file path: $(pwd)/${0}

gradle clean assembleDebug

zip -r $(pwd)/app/build/outputs/apk/ReleaseApks.zip $(pwd)/app/build/outputs/apk/*.apk

java -jar $(pwd)/GDUSample.jar

sleep 90

  • 撰寫完後,我們就可以執行該檔案,就可以直接為我們建置apk,最後直接上傳至Google Drive了,如下圖所示。




Sample Code(Google Drive Uploader): https://github.com/xavier0507/GoogleDriveUploaderSample.git

Sample Code(Build Variants Sample): https://github.com/xavier0507/Build-Variants-Sample.git