これは何?

AndroidアプリをCircleCIでCIする。」のUI Testの書き方について説明した記事です。 具体的にはサンプルアプリUI Testであるこれの説明です。

概要

AndroidでUI Testを書く

Androidが公式にサポートしているUI Testのツールは EspressoUI Automator の2種類あります。 それぞれ以下の特徴がありますので「どちらを使うか?」については「どちらも併用して使う」のがいいように感じています。

Espresso

「to write concise, beautiful, and reliable Android UI tests」と公式サイトには説明されています。特定のアプリのUIに対してのスクリプトで動作をさせることを可能にするテストフレームワークです。単一のアプリの操作を自動化する場合に使うとよいでしょう。Google社が開発していますので、Anroidの公式のテストツールと言ってよいでしょう。
サンプルアプリではアプリの操作のすべてをEspressoで書いています。

UI Automator

「suitable for cross-app functional UI testing across system and installed apps.」と公式サイトに説明されている通りで、Espressoと比べると、よりAndroidのOSに近い側に位置しているテストフレームワークで、複数アプリを行き来するよう動作をスクリプトで定義することの可能です。Espressoとは違い、複数のアプリの操作を自動化する場合に使うとよいでしょう。こちらもEspressoと同じくGoogle社が開発していますので、Anroidの公式のテストツールと言ってよいでしょう。
サンプルアプリではスクリーンショットの撮影、Permissionリクエストのウィンドウの操作の2つをUI Automatorで書いています。

Espresso、UI AutomatorでTestを書く準備をする

以下の2つのファイルに追加して準備完了です。

build.gradle に以下を追加

....
allprojects {
....
  tasks.matching {it instanceof Test}.all {
    testLogging.events = ["failed", "passed", "skipped"]
  }
....
}
....

> app/build.gradle に以下を追加

....
dependencies {
....
  // for connected Android test
  androidTestUtil 'com.android.support.test:orchestrator:1.0.2'
  androidTestImplementation 'com.android.support.test:runner:1.0.2'
  androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
  androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'
  androidTestImplementation 'com.android.support.test.uiautomator:uiautomator-v18:2.1.3'
  androidTestImplementation 'com.android.support.test:rules:1.0.2'
  androidTestImplementation 'junit:junit:4.12'
....
}

テストを書く

テストはapp/src/androidTest/[Package名]以下に書いていきます。
今回のパッケージ名は com.example.uitestsample ですので app/src/androidTest/java/com/example/uitestsample/ 以下にコードを書いていきます。
ファイル作成の粒度はActivity毎、Fragment毎、機能毎等、自由にまとめてしまって問題ありません。 サンプルアプリではActivity毎でまとめてMainActivityInstrumentedTest.ktに書いています。

前準備

@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 26)
@LargeTest
class MainActivityInstrumentedTest {

    private val _packageName = "com.example.uitestsample"
    private val mUTs: UiTestUtils = UiTestUtils()
    // ^^^ ツールをインスタンス化 ^^^

    @Rule
    @JvmField
    val mActivityTestRule: ActivityTestRule<MainActivity> =
        ActivityTestRule(MainActivity::class.java)

    @Rule
    @JvmField
    val cGrantPermissionRule: GrantPermissionRule =
        GrantPermissionRule.grant(android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
    // ^^^ スクリーンショット保存の為にSTORAGEへのアクセスを強制的に許可 ^^^

    @Rule
    @JvmField
    val screenshotRule = ScreenshotTakingRule(this.mUTs)
    // ^^^ Test失敗時をスクリーンショットを撮影するように指定 ^^^

    @Before
    fun setup() {
        this.mUTs.setActivity(mActivityTestRule.activity)
    }

    @After
    fun teardown() { }

....

}

「Test失敗時をスクリーンショット」の動作は uitestutils/UiTestUtils.kt の最後に定義されています。

....
class ScreenshotTakingRule(mUTs: UiTestUtils) : TestWatcher() {
    private var mUTs = mUTs

    override fun failed(e: Throwable?, description: org.junit.runner.Description?) {
        super.failed(e, description)
        val path = mUTs.screenShot("FAIL-$description")
        mUTs.log_d(">>> !!! TEST FAILED !!! <<< ScreenShot Taken method=[$description] filename=[$path]")
    }
}

テストケース

サンプルアプリではテストケースは以下の3つです。 「ケースとして足りない!」とかツッコミはありかもしれませんが、UI Testを動かすことを目的としていますのでご容赦ください。

  • useAppContext():テストしているアプリのパッケージ名を確認
  • checkTextHelloWorld():アプリ起動時、中心の「Hello World!」の表示を確認
  • checkButtonIncrementFloating():画面右下のボタンをタップすると表示している数字がカウントアップしSnackbarが表示され、またメニューからResetするとゼロになることを確認

checkButtonIncrementFloating() のポイントをインラインで説明します。
Testのコード全体はこちらになりますので合わせて御覧ください。

....
@Test
fun checkButtonIncrementFloating() {
    // false にすることでTestが成功した場合でもスクリーンショットを残せます。
    // デフォルトでは、Testが成功するとスクリーンショットは全削除となります。
    this.mUTs.prepareScreenShot(false)

....

    val willTap = 5
    // 右下の赤色のFabを指定
    val incrementButton =  withId(R.id.increment_fab_text)

    // カウンターを増加させて表示が想定通りであるか確認する
    for(i in 1..willTap) {
        // スクリーンショットを撮影
        mUTs.screenShot("", "BEFORE >>> カウンター:インクリメント IDX=[$i]")
        // 指定した赤色のFabをタップ
        onView(incrementButton).perform(click())

        // Permissionリクエストが出てたら許可(このテスト中は出ないはず)
        mUTs.allowPermissionsIfNeeded()

        // スクリーンショットを撮影
        mUTs.screenShot("", "AFTER >>> カウンター:インクリメント IDX=[$i]")
        // 要素内の文字列を取得
        actualCount = this.mUTs.getText(withId(R.id.main_content_text))
        // ログを残す
        this.mUTs.log_d("[Counter SEQ] 🍏🍎 expected=[$i] actual=[$actualCount]")
        // 文字列をAssert
        assertEquals("[Counter SEQ] 🍏🍎", i.toString(), actualCount)

        // Snackbarの文言チェック
        val snackBarTapped = allOf(withId(android.support.design.R.id.snackbar_text), withText("Tapped $i times."))
        // Snackbarがから消えるのを待つ
        waitForSnackbarDisappear(snackBarTapped)
        // Sleepする
        this.mUTs.sleep("SHR")
    }

....

    // this.mUTs.prepareScreenShot() に false をセットしていなければ
    // スクリーンショットを削除する()
    this.mUTs.removeSuccessScreenShots()

....

}
....

IDが指定されていないエレメントの指定方法

エレメントに対してIDが振られている場合は大抵の場合そのIDを使うことで指定することが可能ですが、 指定されていない場合は Layout Inspector公式ドキュメント)で階層構造を取得 して解析をしてから、以下のように指定を行います。

Fragmentの重なり方が操作によって変化する場合があり、表示は同じでも階層が違う場合が多々発生します。 ですので、Layout Inspectorで階層構造を取得するときはUI Testでのシナリオ通りに一度画面を遷移させて、それから取得すると良いでしょう。

おわりに

iOS標準のUI TestツールであるXCTestに比べると 癖が少し強いです。とっつきにくいところもありますが、そこまで難しくはないので気になっている場合は挑戦してみてください。
自分の作ったアプリが自動で動くのを見るのも楽しいと思います。

Reference

これは何?

AndroidアプリをCircleCIでCIする。」のUI TestをCircleCIから実行する方法について説明した記事です。 サンプルアプリのUI Testを走らせるFirebase Test Lab(UI Testを実行するインフラ)について解説していきます。

概要

Ciの流れでUI Testが実行されている箇所

AndroidアプリをCircleCIでCIする。」のCIの流れでは (3) UI Test でUI Testを実行、 (4) Report でレポートアップロードしています。

ここで使うコードは以下の通りです。

ポイント、条件など

CIで使うCircleCIは2.0からAndroidのエミュレータを動かすことができなくなったことから、 CircleCI単体でのUI Testの実行が不可能になりました。(公式ドキュメント) ですので、UI Testを含めるとすると、AndroidアプリのCIは外部サービスを利用することが必須となっています。

今回は公式ドキュメントにオススメされている Firebase Test Labを使うことにしました。

Firebase Test Labとは

Firebase Test LabはFirebaseがのサービスの1つとして提供しているクラウドでUI Testを行うプラットフォームです。Android(Espresso、UI Automator)、iOS(XCTest)で書かれたテストの実行に対応しています。

(詳しくは「AndroidアプリをCircleCIでCIする。」のFirebase Test Labとはを御覧ください。)

公式ドキュメントはこちらです。

それではアカウント等の準備を説明します。

料金体系(2019年7月現在)

料金は無料枠が、物理デバイスでのテスト実行5回まで、Virtual(仮想)デバイスでのテスト実行10回まで。
無料枠を超えると、物理デバイス1台1時間$5、Virtual(仮想)デバイス1台1時間$1となります。 また、テスト実行時間の上限もあり、物理デバイスが30分、Virtual(仮想)デバイスが60分と決められていますので、 Firebase Test Labでテストを実行する場合はそこに収まるようにテストケースを考えましょう。
この時間はデフォルトでは物理デバイスもVirtual(仮想)デバイスも同じく15分で設定されています。

詳細はここのTest Labの項目を御覧ください。

> Firebaseのアカウントの準備

console.firebase.google.comからアカウントを作成します。

> GCPのアカウントとプロジェクトを準備

Circle CIからは gcloud コマンドを使って Firebase Test Lab 実行するので、GCPのアカウントも必須となります。 またプロジェクトの必要となりますので作成していきます。

続いて、gcloud コマンドはこの公式ドキュメントの通りインストールを行ってください。
最後にAndroidのBuild環境をセットアップしてください。Android Studioをインストールするのがよいと思います。

> コマンドから動かしてみる

まずは、ターゲットのプロジェクトを指定を行います。

# コマンドラインでGCPにログイン
$ gcloud auth;

# 設定可能なプロジェクトをリストする
$ gcloud projects list;
PROJECT_ID                 NAME                   PROJECT_NUMBER
my-test-project-00-2xxxx5  My-Test-Project-00     6xxxx4xxxxx6

# 「my-test-project-00-24690」をプロジェクトとして指定する
$ gcloud config set project my-test-project-00-2xxxx5;

# 設定内容を確認する(account, projectが指定した内容であるか確認)
$ gcloud config list;
[compute]
region = us-central1
zone = us-central1-c
[core]
account = [email protected]
disable_usage_reporting = True
project = my-test-project-00-2xxxx5

Your active configuration is: [default]

> ローカルで動作させてみる

# コードをCloneする
$ git clone https://github.com/ryoyakawai/uitest_sample_android.git;
$ cd uitest_sample_android/;

# Build
# Emulatorが動作していない場合エラーとなりますが、以下の2つが生成されていればOKです。
# - app/build/outputs/apk/debug/app-debug.apk
# - app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
$ ./gradlew connectedAndroidTest;

# 環境変数を指定する
$ GOOGLE_PROJECT_ID="my-test-project-00-2xxxx5";
$ BK_OBJ_NAME="${GOOGLE_PROJECT_ID}/uitest-$(date "+%Y%m%d_%H%M")";

# コマンドを改行するときは末尾のスペースを忘れずに入れてください。
$ gcloud firebase test android run \
 --type instrumentation \
 --app ./app/build/outputs/apk/debug/app-debug.apk \
 --test ./app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
 --test-targets "class com.example.uitestsample.MainActivityInstrumentedTest" \
 --results-dir $BK_OBJ_NAME \
 --results-bucket cloud-test-${GOOGLE_PROJECT_ID} \
 --directories-to-pull /sdcard/uitest/ \
 --device model=Pixel2,version=26,locale=en_US,orientation=portrait \
 --use-orchestrator \
 --timeout 120s;

# 無事に開始されると以下が出力さる
Have questions, feedback, or issues? Get support by visiting:
  https://firebase.google.com/support/

Uploading [./app/build/outputs/apk/debug/app-debug.apk] to Firebase Test Lab...
Uploading [./app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk] to Firebase Test Lab...
Raw results will be stored in your GCS bucket at [https://console.developers.google.com/storage/browser/cloud-test-my-test-project-00-2xxxx5/my-test-project-00-2xxxx5/uitest-20190716_1603/]

Test [matrix-1gpbgxrsx8fag] has been created in the Google Cloud.
Firebase Test Lab will execute your instrumentation test on 1 device(s).
Creating individual test executions...done.

Test results will be streamed to [https://console.firebase.google.com/project/my-test-project-00-2xxxx5/testlab/histories/bh.xxxxxxxxxxxx/matrices/8xxxxxxxxxxxx11].
16:17:55 Test is Pending
16:18:23 Starting attempt 1.
16:18:23 Test is Running
16:19:17 Started logcat recording.
16:19:17 Preparing device.
16:19:52 Logging in to Google account on device.
16:19:52 Installing apps.
16:20:06 Retrieving Pre-Test Package Stats information from the device.
16:20:06 Retrieving Performance Environment information from the device.
16:20:06 Started crash detection.
16:20:06 Started crash monitoring.
16:20:06 Started performance monitoring.
16:20:19 Started video recording.
16:20:19 Starting instrumentation test.
16:20:53 Completed instrumentation test.
16:21:07 Stopped performance monitoring.
16:21:14 Stopped crash monitoring.
16:21:14 Stopped logcat recording.
16:21:14 Retrieving Post-test Package Stats information from the device.
16:21:14 Logging out of Google account on device.
16:21:21 Done. Test time = 36 (secs)
16:21:21 Starting results processing. Attempt: 1
16:21:28 Completed results processing. Time taken = 6 (secs)
16:21:28 Test is Finished

Instrumentation testing complete.

More details are available at [https://console.firebase.google.com/project/my-test-project-00-2xxxx5/testlab/histories/bh.xxxxxxxxxxxx/matrices/8xxxxxxxxxxxx11].
┌─────────┬──────────────────────────┬─────────────────────┐
│ OUTCOME │     TEST_AXIS_VALUE      │     TEST_DETAILS    │
├─────────┼──────────────────────────┼─────────────────────┤
│ Passed  │ Pixel2-26-en_US-portrait │ 3 test cases passed │
└─────────┴──────────────────────────┴─────────────────────┘

コマンドに出力されるFirebaseのURLにアクセスすると結果が見られます。以下の1行のように表示されているはずで、ブラウザからアクセスすると下図が表示されます。

Test results will be streamed to [https://console.firebase.google.com/project/my-test-project-00-2xxxx5/testlab/histories/bh.xxxxxxxxxxxx/matrices/8xxxxxxxxxxxx11]

静止画、動画を一覧で表示することができるので、テストで何がされていたかも後から確認することが可能です。 ちなみに、デフォルトで動画は撮影してくれる設定になっていますが、静止画に関しては自前で撮影する処理を書き、タイミングを決めてテストスクリプトに埋め込む必要があります。 テスト失敗時の静止画も自前で設定する必要がありますが、今回のサンプルアプリのUiTestUtilsに含んでいますので試してみたい場合は利用してみてください。

おわりに

CIの流れの中ではもちろん、また「日々の開発と並行してテストしたい場合にローカルで環境を用意する」ことはコスト的に厳しかったり、 「他端末の実デバイスを使ってまずはテストを動かしてみたい」というのも同じくコスト的に厳しいことが少なくないと思います。 全ては無理かもしれませんが、そんなときに心配の塊の1部だけでも取り除くべきFirebase Test Labを使う、というのもとても有効だと思います。
気になっている方は使ってみてはいかがでしょうか?

Reference

これは何?

AndroidアプリをCircleCIでCIする。」のUnit Testを説明した記事です。 サンプルアプリのUnit Testについて解説していきます。

概要

CIの流れでUnit Testが実行されている箇所

AndroidアプリをCircleCIでCIする。」のCIの流れでは (2) Build で実行されています。

ここで使うコードは以下の通りです。

公式ドキュメントはこれです。

ポイント、条件など

Unit Testについて(AndroidアプリをCircleCIでCIする。)にも書いていますがポイントは以下です。

  • MVP(Model-View-Presenter)のアーキテクチャに対してのUnit Testを実行する
  • JUnitを使ってUnit Testを実行する
  • MockitoでViewをモックする
  • HTTPでのアクセスをモックする
  • テストする場所は以下の図の Point for Unit Testing

アーキテクチャはMVPに限っている訳ではありませんが、Interfaceを定義しているとテストを書くのが楽になることから ここではMVPを採用しています。

テストのシナリオ

アプリの機能としては無駄に実装されたJSONPlaceholder のREST APIに接続をしてJSONを取得し、Viewに反映させる部分の動作に関してテストを実施します。
具体的に説明すると上図の Data Interaction をモックしてJSONを固定し、UI Behavior をモックして 指定されている振る舞いを行うか、を確認します。
よって、今回はこのシナリオを確認する為に以下の2つのテストのケースを用意しました。

  • HTTPレスポンスコード200でJSONを正しく受け取った場合の値の確認とViewに対する動作確認
  • HTTPレスポンスコード500を受け取った場合のViewに対する動作確認

準備

build.gradleに追記、org.mockito.plugins.MockMaker の作成の2つが必要です。

build.gradleに追記

app/build.gradle に以下を追記します。

....
dependencies {
....
    // Unit and UI Test
    testImplementation 'junit:junit:4.12'
    testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
    testImplementation "com.android.support:support-annotations:${android.defaultConfig.targetSdkVersion}"
    testImplementation 'org.robolectric:robolectric:4.0'
    testImplementation 'com.nhaarman:mockito-kotlin-kt1.1:1.5.0'
    testImplementation 'com.squareup.retrofit2:converter-moshi:2.2.0'
    testImplementation 'com.squareup.retrofit2:retrofit-mock:2.3.0'
    testImplementation 'com.squareup.okhttp3:mockwebserver:3.10.0'
....
}
....

build.gradle に以下が書かれていることが前提です。

....
buildscript {
....
    ext.kotlin_version = '1.3.31'
....
}
....

org.mockito.plugins.MockMaker を作成

app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker を新規で作成して、以下の1行を書き込みます。

mock-maker-inline

Unit Testを書く

> REST APIをモックして出力を固定する

Data Interaction の部分をモックして出力を固定します。具体的にはJSONPlaceholderのREST APIの出力を固定する為に HTTPのクライアントOkHttpをモックします。 扱いやすいように、 Unit Test本体 と、 モック を1つのファイルにまとめちゃっています。

MockServerDispatcherのクラスがそれになります。
ここでは、Resp200Resp400Resp500 の3つのClassを定義することで、以下の用に出力を固定します。

  • Resp200:正常系の場合で、JSONを返す
  • Resp400:異常系で、HTTP Statusの400を返す
  • Resp500:異常系で、HTTP Statusの500を返す

> Unit Testを書く

2つのテストケースのうちの「HTTPレスポンスコード200でJSONを正しく受け取った場合の値の確認とViewに対する動作の確認」 の説明を、以下にインラインで行います。。 以下は、

class MainActivityUnitTest {
    private var mMockTestUtils = UnitTestUtils()

    @Test
    fun sampleUnitDataFetchSuccessTest() {
        // vvv 正常系でJSONを返すよう指定
        mMockTestUtils.mockServerBehaviorSwitcher = {
            MockServerDispatcher().Resp200()
        }
        val expectedResponse = MockServerDispatcher().mockedResponse

        var mMainActivityPresenter = MainActivityPresenter()
        // vvv Viewに対する動作の確認の為にMainActivityViewContract()のClassをモック
        var mMainActivityViewContract = mock<MainActivityViewContract>()

        // vvv PresentorにモックしたMainActivityViewContract()を叩かせるようにセット
        mMainActivityPresenter.setView(mMainActivityViewContract)

        // vvv 非同期での処理を同期で動作させるように変更
        mMockTestUtils.prepareRxForTesting()

        // vvv ここでMockしたサーバを起動する
        mMockTestUtils.startMockServer()

        // vvv 以下の2行で、サンプルアプリの接続先をMockしたサーバに入れ替える
        var mApiConnection = mMockTestUtils.setupMockServer()
        setConnection(mApiConnection)

        // vvv PresenterのMethodを叩いて、動作させる
        mMainActivityPresenter.getJsonSampleResponse()
        // vvv MainActivityViewContractのhandleSuccess()が叩かれれ、
        // 固定したデータ(JSON)の取得ができているかを確認
        argumentCaptor<Array<SinglePostResponse>>().apply {
            verify(mMainActivityViewContract, times(1)).handleSuccess(capture())

            for (i in 0 until expectedResponse.size) {
                val expected = expectedResponse[i]
                val actual = this.firstValue[i]
                mMockTestUtils.assertDataClass(expected, actual)
            }

        }
        // vvv ここでMockしたサーバを停止
        mMockTestUtils.shutdownMockServer()
    }
....
}

実行してみる

実行方法には「コマンドラインから」、また「Android Studioから」の2つがあります。

> コマンドラインから実行

Unit Testの全てを実行する

$ ./gradlew testDebugUnitTest;

指定したClass、MethodのUnit Testを実行る

// Classを指定
// --tests のパラメータとして [Package Name].[Class Name] を指定
$ ./gradlew testDebugUnitTest --tests com.example.uitestsample.MainActivityUnitTest;

// ClassとMethodを指定
// --tests のパラメータとして [Package Name].[Class Name].[Method Name] を指定
$ ./gradlew testDebugUnitTest --tests com.example.uitestsample.MainActivityUnitTest.sampleUnitDataFetchSuccessTest;

> Android Studioから

@Testのアノテーションかを書くとTestとして認識されます。 するとAndroid Studioだと下図の赤丸のように、その左側に ▶︎(再生マーク)が表示されるので、 それをクリックすると実行することが可能です。

おわりに

「Cloneしたらすぐに試せる」を目標に書きましたので、興味を持たれた方は試してみていただえると嬉しいです。
間違ってる!とかありましたらPR、またご指摘ください。

これは何?

AndroidアプリのCI(継続的インテグレーション)環境を作って運用をしたときの経験を書き出したものです。 2019年7月時点でのものになりますので、時間経過によっては動作しない可能性もありますので予めご了承ください。

更新履歴

概要

AndroidアプリのGitHubへのPushから、CircleCIでビルド、単体テスト、UIテスト(UI Animator & Espresso on Firebase Test Lab)、 そしてDeployGateへアプリをデプロイする、までの一連の流れの雛形のようなものだと考えてください。
また記事の内容は以下のように分割して書いていく予定です。

この記事は「テストケース、詳細ははともあれ、CircleCIでCIを回してみる」についてとなります。
動作に必須な設定等の説明をして、ローカル環境(手元のPC)からコマンドを使って手動で動作させ正常に動作するかすかの確認を行い、 その後に一連のビルドのプロセスをCircleCIで動かす、というの流れで説明します。

ビルドの流れとインフラ

以下の図の流れでビルドからデプロイまで行います。

  1. 開発者がGitHubにコードをCommitしPushする
  2. CircleCIでビルドのプロセスが開始され、Unitテストが実行される
  3. UIテスト実行の為、CicleCIがFirebaseに向けてアプリ、テストケースを配信しUIテストを実行する
  4. Firebase Test LabでUIテストが実行が完了したらレポートをCircleCIに配信する
  5. DeployGateに向けてアプリを配信する

利用するインフラ

上記のプロセスを実行する為に、以下のインフラを利用します。おなじみの名前ばかりかもしれませんが・・・

  • GitHub

    Gitで操作するリポジトリを提供するクラウドサービス

  • CircleCI

    CIを行ってくれるクラウドサービス

  • Firebase Test Lab

    Google社が提供するmBaaSが提供するサービスの1つで、UIテスト(Espresso、UI Automator 2.0、XCTest)をクラウドで行うサービス

  • Deploy Gate

    ストア(Google Play、App Store)を通さないアプリ配布を実現するサービス(ベータテスト等に利用できる)

今回利用するAndroidのサンプルアプリ

> サンプルアプリのコード

Unit Test、UI Testを行う為に強引に実装している部分があります。

> 機能の説明

サンプルアプリの機能は以下の通りです。

  • 画面右下の赤色のFabをタップするとデバイスへのファイル書き込みの許可を求められる。
  • デバイスへのファイル書き込みの許可の状態に関係なく、画面中央の文字列「Hello World!!」が「1」に変化する
  • 更に、画面右下の赤色のFabをタップすると、タップ毎に1つづつインクリメントされた数字が表示される
  • 画面右上の3点リーダをタップすると「Reset Counter」のボタンが出現し、タップするとカウンタが「0」に変化する

こんな↓動作をするアプリです。

サンプルアプリの実装のアーキテクチャ

サンプルアプリのアーキテクチャはMVP(Model-View-Presenter)で構成されていて、Activityは1つです。
また今回のサンプルアプリの仕様(上記)ですと、Model(DB、API等のデータソース)が必要のないアプリになってしまいますが、 Unit Testの為、外部のREST APIへ接続を行い、データを取得しConsole出力をするロジックが無駄に実装しています。
アーキテクチャは図にすると以下のような構成です。図内では、Classが1つのブロックになっていて、ブロックの上部に白文字はInterfaceです。 例えば、MainActivityPresenterはMainActivityPresenterContractのInterfaceで構成されたClass、 よって、MainActivityInteractorはInterfaceを規定していないClassであることを表しています。

それでは、できるだけサクッとCircleCIでCIを回してみましょう。

事前準備:リポジトリを作成する

実際に動作させる場合は、上記のURLのコードをFork等をして自前で専用のリポジトリGitHubにご用意ください。

Unit Testについて

(関連記事「AndroidアプリでのUnit Testについての解説」)

JUnitを使ってUnit Testを実行します。AndroidでのUnit Testの定番です。 モックはMockitoを使います。 サンプルアプリのUnit TestはPresenterとやり取りを横取りする形で行います。 上の図の 「Point for Unit Testing」 と書かれた矢印のポイントがそこです。

Unit Testの概要

コードはこのディレクトリに配置しています。

Unit Testのスクリプトはこのファイルです。

共通で使うであろう機能をMethod化して集めたClassがこちら。

テストとしては、先程説明したModelにアプリの動作に対しては無駄に実装したREST APIへ接続するロジックを使います。 REST APIからデータを取得し、取得したデータを元に正しくViewに反映される動作をするかの確認を行うのが目的です。
サンプルアプリの接続先REST APIはJSONPlaceholderです。接続するURLは/comments?postId=1で、postIdが同一であれば常に同じ値のJSONを返してくれます。常に同じJSONを返してくれるとはいえ、Unit Testではより確実性を高めたい、つまり、相手のサーバの状態に関係なく確実に同じJSONを取得できることを保証したいです。ですので、このUnit TestではMockitoを使ってJSONPlaceholderのAPIをMock(モック)することで確実に同一のJSONを受け取れるようにしています。

テストのケースは2つです。その内容は以下の通りです。

  • HTTPレスポンスコード200でJSONを正しく受け取った場合の値の確認とViewに対する動作の確認
  • HTTPレスポンスコード500を受け取った場合のViewに対する動作確認

なお、Unit Testの書き方(お作法)、テストケース詳細は別エントリのAndroidアプリでのUnit Testについての解説で説明しています。

Unit Testをローカル環境で動作させてみる

手元で動作させてみましょう。Terminalでコードのトップに移動して以下のコマンドを実行すると、こんな出力が出てくるはずです。

$ https://ryoyakawai.com/blog/gradlew :app:testDebugUnitTest;

> Task :app:testDebugUnitTest
com.example.uitestsample.MainActivityUnitTest > sampleUnit500ServerErrorTest PASSED
com.example.uitestsample.MainActivityUnitTest > sampleUnitDataFetchSuccessTest PASSED
com.example.uitestsample.MainActivityUnitTest > sampleUnit400BadRequestTest PASSED

> Task :app:testReleaseUnitTest
com.example.uitestsample.MainActivityUnitTest > sampleUnit500ServerErrorTest PASSED
com.example.uitestsample.MainActivityUnitTest > sampleUnitDataFetchSuccessTest PASSED
com.example.uitestsample.MainActivityUnitTest > sampleUnit400BadRequestTest PASSED

BUILD SUCCESSFUL in 8s
40 actionable tasks: 10 executed, 30 up-to-date

「BUILD SUCCESSFUL in XXs」 が出たら Unit TestはテストケースをすべてSuccessで終了した という意味になります。また、ここでWarning等のメッセージが出た場合、できる限り修正してメッセージが表示されないようにすることをオススメします。
これでUnit Testの準備は完了です。

UI Testについて

EspressoUI Automatorを使っています。それぞれの特徴は以下の通りです。

Espresso

「to write concise, beautiful, and reliable Android UI tests」と公式サイトには説明されています。特定のアプリのUIに対してのスクリプトで動作をさせることを可能にするテストフレームワークです。単一のアプリの操作を自動化する場合に使うとよいでしょう。Google社が開発していますので、Anroidの公式のテストツールと言ってよいでしょう。

UI Automator

「suitable for cross-app functional UI testing across system and installed apps.」と公式サイトに説明されている通りで、Espressoと比べると、よりAndroidのOSに近い側に位置しているテストフレームワークで、複数アプリを行き来するよう動作をスクリプトで定義することの可能です。Espressoとは違い、複数のアプリの操作を自動化する場合に使うとよいでしょう。こちらもEspressoと同じくGoogle社が開発していますので、Anroidの公式のテストツールと言ってよいでしょう。

UI Testの概要

コードはこのディレクトリに配置しています。

UI Testのスクリプトはこのファイルです。

共通で使うであろう機能をMethod化して集めたClassがこちら。

テストのケースは3つです。その内容は以下の通りです。

  • パッケージ名を確認する
  • アプリ起動時の画面の文字列の確認をする
  • アプリ起動後、各ボタンが正しく機能し、画面表示が仕様通り更新されるかを確認する

なお、UI Test(Espresso、UI AUtomator)の書き方(お作法)、テストケース詳細は別エントリの「Espresso, UI Automator„ÅßAndroid„ÅÆUI Test„ÇíÊõ∏„Åè」で説明していますので合わせて御覧ください。

UI Testをローカル環境で動作させてみる

手元で動作させてみましょう。Terminalのコマンドラインからコードのトップディレクトリに移動して以下のコマンドを実行すると、こんな↓が出力が出てくるはずです。

$ ./gradlew :app:connectedAndroidTest; // ← ./gradlew cAT でもOK

> Task :app:connectedDebugAndroidTest
01:52:09 V/ddms: execute: running am get-config
01:52:09 V/ddms: execute 'am get-config' on 'emulator-5554' : EOF hit. Read: -1
01:52:09 V/ddms: execute: returning
01:52:09 D/app-debug.apk: Uploading app-debug.apk onto device 'emulator-5554'
01:52:09 D/Device: Uploading file onto device 'emulator-5554'
....
01:52:13 V/ddms: execute: running pm install -r -t "/data/local/tmp/test-services-1.0.2.apk"
01:52:13 V/ddms: execute 'pm install -r -t "/data/local/tmp/test-services-1.0.2.apk"' on 'emulator-5554' : EOF hit. Read: -1
01:52:13 V/ddms: execute: returning
01:52:13 V/ddms: execute: running rm "/data/local/tmp/test-services-1.0.2.apk"
01:52:13 V/ddms: execute 'rm "/data/local/tmp/test-services-1.0.2.apk"' on 'emulator-5554' : EOF hit. Read: -1
01:52:13 V/ddms: execute: returning
01:52:13 D/app-debug-androidTest.apk: Uploading app-debug-androidTest.apk onto device 'emulator-5554'
01:52:13 D/Device: Uploading file onto device 'emulator-5554'
....
androidx.test.internal.runner.junit3.DelegatingFilterableTestSuite > [API_27_Pixel_2(AVD) - 8.1.0] SKIPPED
01:52:20 V/InstrumentationResultParser: INSTRUMENTATION_STATUS_CODE: -3
01:52:21 V/InstrumentationResultParser: INSTRUMENTATION_STATUS: class=androidx.test.internal.runner.junit3.DelegatingTestSuite
01:52:21 V/InstrumentationResultParser: INSTRUMENTATION_STATUS: current=3
01:52:21 V/InstrumentationResultParser: INSTRUMENTATION_STATUS: id=AndroidJUnitRunner
01:52:21 V/InstrumentationResultParser: INSTRUMENTATION_STATUS: numtests=8
....
01:53:07 V/InstrumentationResultParser: Time: 48.21
01:53:07 V/InstrumentationResultParser:
01:53:07 V/InstrumentationResultParser: OK (3 tests)
....
01:53:07 V/ddms: execute: returning
01:53:07 V/ddms: execute: running pm uninstall com.example.uitestsample.test
01:53:07 V/ddms: execute 'pm uninstall com.example.uitestsample.test' on 'emulator-5554' : EOF hit. Read: -1
01:53:07 V/ddms: execute: returning
01:53:07 V/ddms: execute: running pm uninstall com.example.uitestsample
01:53:07 V/ddms: execute 'pm uninstall com.example.uitestsample' on 'emulator-5554' : EOF hit. Read: -1
01:53:07 V/ddms: execute: returning

BUILD SUCCESSFUL in 1m 1s
51 actionable tasks: 10 executed, 41 up-to-date

Unit Testのときと同じく、「BUILD SUCCESSFUL in XXs」 が出たら UI TestはテストケースをすべてSuccessで終了した という意味になります。また、ここもUnit Testのときと同様にWarning等のメッセージが出てきたら、でいる限り修正することをオススメします。

Firebase Test Labでテストを行う

Firebase Test Labとは

Firebase Test LabはFirebaseがの1つのサービスとして提供されているクラウドでUI Testを行うプラットフォームです。Android(Espresso、UI Automator)、iOS(XCTest)で書かれたテストの実行に対応しています。操作は以下の2つの方法が提供されています。

どちらもの方法を使っても結果はブラウザ上で動作しているFirebaseのConsole(下図↓)から閲覧が可能になります。

ここではGoogle Cloud SDKのCLIからCloud Testing APIを使う方法で実行します。 Google Cloud SDKのCLIから使う場合は以下の2つのAPIを有効にする必須です。必ず以下のリンクから利用するプロジェクトで有効にしてください。

(Firebase Test Labの設定については「Firebase Test LabでUI Testを実行する」で詳しく説明しています。)

Firebase Test Labをローカル環境で動作させてみる

FirebaseでのProjectの設定、GCPのアカウントの準備、Google Cloud SDKのCLIは準備は済んでいると仮定します。
また既にローカルでUIテストの動作確認も済んでいますので、早速UIテストをFirebase Test Labで動かしてみます。
Firebase Test LabでUI Testを実行する」でも詳しく説明しています。)

// Cloud Testing APIを有効にしたアカウントでログインし、CLIの向き先Projectを切替える
$ gcloud auth;
$ gcloud config set project [PROJECT ID];

続いて、Cloud Testing APIでUIテストを実行する為に以下をコンソールで実行します。
環境変数で変数を指定して実行していますが、ここではテキストを入力してもOKです。 CircleCIでは環境変数で指定することが望ましいのでそれに習っています。 この記事内の他の項目でも環境変数を使う理由は同一です。

$ TIME=$(date "+%Y%m%d_%H%M");
$ BK_OBJ_NAME="[保存するバケットのディレクトリ名]/${TIME}[今回のテストを保存するディレクトリ]";
$ GOOGLE_PROJECT_ID="[PROJECT ID]";

// コマンドを改行するときは末尾のスペースを忘れずに入れてください。
$ gcloud firebase test android run \
 --type instrumentation \
 --app ./app/build/outputs/apk/debug/app-debug.apk \
 --test ./app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
 --test-targets "class com.example.uitestsample.MainActivityInstrumentedTest" \
 --results-dir $BK_OBJ_NAME \
 --results-bucket cloud-test-${GOOGLE_PROJECT_ID} \
 --directories-to-pull /sdcard/uitest/ \
 --device model=Pixel2,version=26,locale=en_US,orientation=portrait \
 --use-orchestrator \
 --timeout 120s;

実行開始が成功するとFirebaseのConsoleにこんな形↓で1行追加されます。

テストが終了しするとこのような出力がコンソールにされます。

Have questions, feedback, or issues? Get support by visiting:
  https://firebase.google.com/support/

Uploading [./app/build/outputs/apk/debug/app-debug.apk] to Firebase Test Lab...
Uploading [./app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk] to Firebase Test Lab...
Raw results will be stored in your GCS bucket at [https://console.developers.google.com/storage/browser/[PROJECT ID]/cloud-test-uitest-sample-android/20190705-xxxxxx02/]

Test [matrix-3dp8juo0wx533] has been created in the Google Cloud.
Firebase Test Lab will execute your instrumentation test on 1 device(s).
Creating individual test executions...done.

Test results will be streamed to [https://console.firebase.google.com/project/[PROJECT ID]/testlab/histories/bh.xxxxxxxxx/matrices/918190477175429xxxx].
16:25:19 Test is Pending
16:25:40 Starting attempt 1.
16:25:40 Test is Running
16:26:42 Started logcat recording.
16:26:42 Preparing device.
16:27:15 Logging in to Google account on device.
16:27:15 Installing apps.
16:27:28 Retrieving Pre-Test Package Stats information from the device.
16:27:28 Retrieving Performance Environment information from the device.
16:27:28 Started crash detection.
16:27:28 Started crash monitoring.
16:27:28 Started performance monitoring.
16:27:42 Started video recording.
16:27:42 Starting instrumentation test.
16:28:21 Completed instrumentation test.
16:28:34 Stopped performance monitoring.
16:28:41 Stopped crash monitoring.
16:28:47 Stopped logcat recording.
16:28:47 Retrieving Post-test Package Stats information from the device.
16:28:47 Logging out of Google account on device.
16:28:53 Done. Test time = 51 (secs)
16:28:53 Starting results processing. Attempt: 1
16:29:00 Completed results processing. Time taken = 7 (secs)
16:29:00 Test is Finished

Instrumentation testing complete.

More details are available at [https://console.firebase.google.com/project/[PROJECT ID]/testlab/histories/bh.xxxxxxxxx/matrices/918190477175429xxxx].
┌─────────┬──────────────────────────┬─────────────────────┐
│ OUTCOME │     TEST_AXIS_VALUE      │     TEST_DETAILS    │
├─────────┼──────────────────────────┼─────────────────────┤
│ Passed  │ Pixel2-26-en_US-portrait │ 3 test cases passed │
└─────────┴──────────────────────────┴─────────────────────┘

出力されている GCS bucketMore details are availabl at として表示されているURLにアクセスするとテスト結果が書き出されているはずです。閲覧はブラウザから可能です。

DeployGateを準備する

ここを参考に、サインアップアプリをアップロード まで済ませましょう。

DeployGateにローカル環境から配信してみる

DeplotGateのAPI keyを取得

DeployGateのサイトにログインをしてhttps://deploygate.com/settingsの最下段に表示されています。

これを環境変数として設定します。 ついでにユーザID(DeployGateのユーザーID)とAPKへのPathも環境変数に設定してしまいましょう。

$ DEPLOYGATE_API_KEY="[取得したAPI key]";
$ USERNAME="[DeployGateのユーザID]";
$ APK_PATH=app/build/outputs/apk/debug/app-debug.apk;

DeployGateに配信する

以下のコマンドで配信します。メッセージをリッチにするために環境変数を少々追加しています。

$ TIME=$(date "+%Y/%m/%d %H:%M");
$ COMMIT_HASH=$(git log --format="%H" -n 1 | cut -c 1-8);
$ curl -F "[email protected]${APK_PATH}" -F "token=${DEPLOYGATE_API_KEY}" -F "message=Build by CircleCI <${COMMIT_HASH}> (${TIME})" https://deploygate.com/api/users/${USERNAME}/apps

このような出力されるはずです。"error":falseと表示されていたら配信成功です。

{"error":false,"results":{"name":"UITest Sample App","package_name":"com.example.uitestsample","labels":{},"os_name":"Android",..../secure.gravatar.com/avatar/410d1a2cc20ac9675664df7de253156b?s=218\u0026d=mm"}}}

DeployGateのウェブ管理コンソール(URLはhttps://deploygate.com/users/[DeployGateのユーザID]/apps/[アプリのPackage名])では、以下のように赤四角のリストにアイテムが追加されているはずです。


ビルドのプロセスをローカル環境から手動で回すことの確認まで行いましたので、CircleCI上で動かしてみます。

CircleCIの設定をする

.circleci/config.ymlが設定ファイルになります。
動作させるには環境変数としてDEPLOYGATE_API_KEYGCLOUD_SERVICE_KEYGOOGLE_PROJECT_IDの設定が必須です。 それぞれの値の取得方法は以下になります。

DEPLOYGATE_API_KEY

DeployGateのサイトにログインをしてhttps://deploygate.com/settingsの最下段に表示されています。 (上記 「DeplotGateのAPI keyを取得」 の項目で説明しているAPI keyと同じです)

GOOGLE_PROJECT_IDGCLOUD_SERVICE_KEY

GOOGLE_PROJECT_IDはJSON形式のファイルの内容をbase64にした値です。

  1. 下↓の左図のように(1)でPROJECT_IDを選択し(ここで選択した文字列がGOOGLE_PROJECT_IDとなります)、(2)のように [IAM & admin] > [Service account] を選択してアカウントを作成します
  2. 次に[IAM & admin]を表示し、下↓の右図のように先程作成したアカウントの右側の鉛筆マークをクリックして、Firebase Test Lab Admin を追加します
  3. 再度[IAM & admin] > [Service account]を表示して、作成したアカウントの右側にある3点リーダをクリックしてJSONフォーマットのキーを作成しダウンロードします

そしてダウンロードしたJSONフォーマットのキーをbase64形式に書き出します。この文字列をCircleCIに環境変数GCLOUD_SERVICE_KEYとして登録してください。

$ base64 -i [PATH TO JSON FILE];

CicleCIに環境変数として登録

取得した3つの値をDEPLOYGATE_API_KEYGOOGLE_PROJECT_IDGCLOUD_SERVICE_KEYを以下の図のように登録します。
対象のプロジェクトを選択し[Settings]を表示して、左ペインのメニューから[BUILD SETTINGS] > [Environment Variables]に保存します。完了すると以下の図のようになります。 画面上は指定した値の最後の4文字のみ表示された状態となります。

リポジトリにPushしてCircleCI上でBuildを動かす

設定は完了しましたので、準備したリポジトリのMasterブランチにコードをPushします。
PushするとCircleCI上でBuildのプロセスが動き始めます。今後はMasterにPush、またはMergeするとBuildプロセスが走るようになります。

実行結果のレポートを閲覧する

ビルドの結果はCircleCIのサイトから確認することが可能です。成功すると以下のようになるでしょう。
また、Unit Testの結果は Artifacts のタブから確認することが可能です。(下図)

UI Testのも確認は可能です。Successの場合はそれでよいのですが、Failしている場合の詳細結果は下図のようにFirebaseのConsoleから確認してください。また、下図の赤丸内の Test Result をクリックするとその他のデータが閲覧可能となっています。

DeployGateへの配信を確認

「DeployGateに配信する」 の項目での結果のようにリストにアイテムが追加されているはずです。

おわりに

AndroidアプリをCircleCI上でCIする流れを説明してきました。この流れでCIを回していきます。長くなってしまいましたが、いかがでしたでしょうか?
この流れを作るのに多くのサイトにお世話になりました。この記事をご覧になっている方々がサクっとCI環境を作成することができることで、 世界を変えるであろう素晴らしいアプリの開発に時間を注ぐことに少しでもお力になれたら嬉しいです。
なお、今回のUI Testはネットワークアクセスに依存する部分が少なかったのですが、そうでない場合は結果が不安定になりがちですので、通信中なのか、通信は完了しているのかについての何らかの目印をつける、だったりその他の工夫が必要になります。そういったところも何らかの形で共有していきたいと思っています。

それでは、引き続き詳細な解説についてもがんばって書いていきますのでよろしくお願いします。

アメリカン中華の代表格かな?って思ってるバーボンチキン。微妙に焦がすのがポイントな料理。たまに無性に食べたくなるので作ってみました。
もとネタのレシピから調味料(特に、砂糖の量!)をアレンジしています。
もしよかったら作ってみてください!白いご飯に載せて食べると美味しいです😋

バーボンチキン(アメリカン中華?)