(Android) 知らない人が作ったアプリのテストを書く

# UI Automatorで他人のアプリのテストを書きました
最近，あるAndroidアプリで「"pink"と検索するとクラッシュする」
ということに気がつきました．

![crash_cap.png](https://qiita-image-store.s3.amazonaws.com/0/80986/0e266a8d-143e-fa26-ba5c-ab28b681227d.png)

そこで，クラッシュするシナリオを再現するテストを書いて，
そのテストコードと共にバグ報告を行いました．

ということで今回は，
UI AutomatorとUI Automator Viewerを使って，
ソースコードが見れないアプリに対して，テストを書く方法について書きます．

# UI Automatorのセットアップ
UI Automatorは，minSDKVersionが18以上のプロジェクトでしか使えないので，
minSDKVersionが18以上のプロジェクトを用意します．
そして，app配下のbuild.gradleに以下のコードを追加します．

```app/build.gradle
android {
    ...
    defaultConfig {
        applicationId "com.ikota.uiautomatorsample"
        minSdkVersion 18
        ...
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

dependencies {
    ...
    androidTestCompile 'com.android.support.test:runner:0.3'
    androidTestCompile 'com.android.support.test:rules:0.3'
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
}
```
では，本題に入ります．

# クラッシュを再現するテストコードを書く
今回，再現したいシナリオはこうです．

前処理 : ホームスクリーンからアプリを起動しておく
1. サーチアクションボタンをクリック
2. 検索窓に"pink"と入力してEnterを押す
3. 遷移後の画面でアプリがクラッシュ

上のシナリオを再現しようとすると，以下のようなコードになります．

`start_target_app()`でアプリを開く前処理を行い，
`search_pink_and_crash()`でテストしたいシナリオをシミュレートします．

```androidTest/java/com.ikota.uiautomatorsample.MyUiAutomatorTest.java
@RunWith(AndroidJUnit4.class)
public class MyUiAutomatorTest extends InstrumentationTestCase {

    private static final String APP_PACKAGE = "com.target.app";
    private UiDevice mDevice;

    private Intent createLaunchIntent(Context context) {
        Intent intent = context.getPackageManager().getLaunchIntentForPackage(APP_PACKAGE);
        assertNotNull("Target app is not found on your device", intent);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        return intent;
    }

    @Before
    public void start_target_app() throws Exception {
        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();

        // Initialize UiDevice instance and go Home screen
        mDevice = UiDevice.getInstance(instrumentation);
        mDevice.pressHome();

        // start target Activity
        Context context = instrumentation.getTargetContext();
        context.startActivity(createLaunchIntent(context));
        mDevice.wait(Until.hasObject(By.pkg(APP_PACKAGE).depth(0)),5000);
    }

    @Test
    public void search_pink_and_crash() throws Exception {
        // 1. Click search button and expand search field
        UiObject search_btn =  ... // How to get target view ???
        search_btn.click();

        // 2. Input search keyword "pink" and go result screen
        UiObject search_field = ... // How to get target view ???
        search_field.setText("pink");
        mDevice.pressEnter();

        // 3. Wait a moment, then app crashes
        // How to detect crash ???

    }

}
```

ここで，コードを書いてみて2つ分からないことが出てきました．

1. ソースの見れない状態で，操作したいUiObject(View)をどうやって指定するのか.
2. クラッシュしたことのassertionをどうやって書くのか．

これらの問題は，UI Automator Viewerを使うことで解決できます．

#UI Automator Viewerで操作したいviewの情報を見る
UI Automator Viewerとは，AndroidSDKに含まれているツールで
`.../android-sdks/tools/uiautomatorviewer `に置いてあります．

それでは実際に，UI Automator Viewerを使ってみます．

```bash
$ cd .../android-sdks/tools
$ ./uiautomatorviewer 
```
上のコマンドでUI Automator Viewerを起動すると，以下のようなwindowが表示されます．

![uiautomator_default.png](https://qiita-image-store.s3.amazonaws.com/0/80986/eb94e7b0-b92d-f4ff-0a4e-6a9787476626.png)

まず，テストで使いたいview(今回ならサーチボタン)をAndroid端末で表示された状態にして，端末をPCに接続しておきます．
次に，UI Automator Viewerの左上のアイコン列の，端から2番目にあるデバイスのアイコンをクリックします．
すると，次のように端末のスクリーンショットがUI Automator Viewer上に表示されます．

![uiautomator_app_cap.png](https://qiita-image-store.s3.amazonaws.com/0/80986/7572e931-b101-7031-7e37-36174ddb0c87.png)

window上のスクリーンショットのサーチアイコンをクリックすると，
画面右下の領域に，クリックしたviewのidやクラス名などのプロパティが表示されます．
同様に検索窓(`search_field`)のプロパティも調べることで，次のようにviewの指定ができるようになりました．

```androidTest/java/com.ikota.uiautomatorsample.MyUiAutomatorTest.java
// 1. Click search button and expand search field
UiObject search_btn = mDevice.findObject(new UiSelector()
        .resourceId("com.target.app:id/action_search"));
search_btn.click();

// 2. Input search keyword "pink" and go result screen
UiObject search_field = mDevice.findObject(new UiSelector()
        .resourceId("com.target.app:id/search_src_text")
        .className("android.widget.EditText"));
search_field.setText("pink");
mDevice.pressEnter();

//3. ...
```

#クラッシュしたかどうかのアサーションを書く
今回は，クラッシュの検知にもUI Automator Viewerを利用します．

方針は，クラッシュ時に出るダイアログのプロパティをUI Automator Viewerで調べておき，
テストではダイアログが表示されたかどうかのアサーションを書く．というものです．

次のコードは，ダイアログに含まれるエラーメッセージを利用して，
クラッシュの有無をアサーションしています．

```androidTest/java/com.ikota.uiautomatorsample.MyUiAutomatorTest.java
// 3. detect crash by checking if error dialog is displayed.
UiObject error_message = mDevice.findObject(new UiSelector()
        .textMatches("^Unfortunately,.*")
        .resourceId ("android:id/message")
        .className  ("android.widget.TextView"));

// 4. Now, below assertion fails. Please fix it !!!
assertFalse("App crashed !!", error_message.exists());
```

#最後に
Androidアプリの実装に関わっていない人も，
UI Automatorを利用してテストを書いてみてはいかがでしょうか ?

それでは，今回のテストコードの完成形と参考リンクを載せて，締めさせて頂きます．

```androidTest/java/com.ikota.uiautomatorsample.MyUiAutomatorTest.java
package com.ikota.uiautomatorsample;

// import ...

@RunWith(AndroidJUnit4.class)
public class MyUiAutomatorTest extends InstrumentationTestCase {

    private static final String APP_PACKAGE = "com.target.app";
    private UiDevice mDevice;

    private Intent createLaunchIntent(Context context) {
        Intent intent = context.getPackageManager().getLaunchIntentForPackage(APP_PACKAGE);
        assertNotNull("Target app is not found on your device", intent);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        return intent;
    }

    @Before
    public void start_target_app() throws Exception {
        Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();

        // Initialize UiDevice instance and go Home screen
        mDevice = UiDevice.getInstance(instrumentation);
        mDevice.pressHome();

        // start target Activity
        Context context = instrumentation.getTargetContext();
        context.startActivity(createLaunchIntent(context));
        mDevice.wait(Until.hasObject(By.pkg(APP_PACKAGE).depth(0)),5000);
    }

    @Test
    public void search_pink_and_crash() throws Exception {
        // 1. Click search button and expand search field
        UiObject search_btn = mDevice.findObject(new UiSelector()
                .resourceId("com.target.app:id/action_search"));
        search_btn.click();

        // 2. Input search keyword "pink" and go result screen
        UiObject search_field = mDevice.findObject(new UiSelector()
                .resourceId("com.target.app:id/search_src_text")
                .className("android.widget.EditText"));
        search_field.setText("pink");
        mDevice.pressEnter();

        // 3. Wait a moment, then app crashes and error dialog is displayed.
        SystemClock.sleep(5000); // In our case, we need to wait crash here
        UiObject error_dialog = mDevice.findObject(new UiSelector()
                .textMatches("^Unfortunately,.*")
                .resourceId ("android:id/message")
                .className  ("android.widget.TextView"));

        // 4. Now, below assertion fails. Please fix it !!!
        assertFalse("App crashed !!", error_dialog.exists());
    }

}
```
#参考URL

- [Testing UI for Multiple Apps | Android Developers](https://developer.android.com/intl/ja/training/testing/ui-testing/uiautomator-testing.html)