View on GitHub
mixi-inc/AndroidTraining
テスト
AndroidTraining - Training course repository for android app development

目次

テストディレクトリの作成

ユニットテスト

テストの設定

テストの実行

IDE

コマンド

テストレポート

description: この章では、ユニットテストとシナリオテストについて解説します。 —

参考:Testing Fundamentals | Android Developers
参考:Activity Testing | Android Developers
参考:Service Testing | Android Developers
参考:Content Provider Testing | Android Developers
参考:UI Testing | Android Developers
参考:What To Test | Android Developers
参考:uiautomator | Android Developers

目次

テストプロジェクトの作成

Android では、アプリ本体のプロジェクトと、テストケース用のプロジェクトが分離されています。
テストプロジェクトのセットアップは、Eclipse 上から実行することができます。

File > New から、Project… または Other… を選択してください(Ctrl + N または Command + N)。

新規プロジェクトの画面から、Android > Android Test Project を選択します。

Launch Test Project Wizard

次に、テストプロジェクトのセットアップをします。
テストプロジェクトの名前と、配置場所を指定します。

Test Project Configuration

次に、テストのターゲットとするアプリ本体のプロジェクトを選択します。

Select target

最後に、テストプロジェクトが利用する SDK のバージョンを選択して完了です。

Select SDK Version

このテストプロジェクトを用いて、ユニットテストや、シナリオテストを記述していきます。

ユニットテスト

Android では、フレームワーク自身がユニットテストのフレームワークを持っています。
このフレームワークは、JUnit3 をベースにしています。

テスト対象に応じて、独自に拡張されたテストケースクラスが用意されています。

AndroidTestCase

一般的な、Android のテストケースクラスです。
Context、特にActivityContextを必要とするクラスのテストをするために作られています。
これによって、リソースへのアクセスも可能となっています。

テストケースは、メソッドごとに定義します。
この時、メソッド名の命名規則として、先頭に必ずtestと記述する必要があります(JUnit3 の仕様)。
また、テストケースは、記述した順に実行されるとは限りません。

テストケースを実行するごとに、前準備と後始末をするためのメソッドも用意されています。
それぞれ、事前準備をするメソッドはAndroidTestCase#setUp()、後始末をするメソッドはAndroidTestCase#tearDown()です。
特に、テスト内でファイル I/O やデータベース接続などのリソースを扱う場合は、後始末をするtearDown()で、必ずリソースのクローズを行うようにします。

以下に、テストのターゲットとするクラスのサンプルと、そのターゲットをテストするサンプルを示します。

public class SampleTestTarget1 {
    public int add(int left, int right) {
        return left + right;
    }

    public int div(int left, int right) {
        if (right == 0) throw new IllegalArgumentException("right operand must not be zero.");
        return left / right;
    }
}
public class MyTestCase extends AndroidTestCase {
    // テストの前準備のメソッド。テストケースの実行ごとに呼ばれる。
    // 事前準備中に何らかの例外が起こる可能性があるので、例外をスローする宣言をする。
    @Override
    protected void setUp() throws Exception {
        super.setUp();
    }

    // テストの後始末のメソッド。テストケースの実行ごとに呼ばれる。
    // 後始末中に何らかの例外が起こる可能性があるので、例外をスローする宣言をする。
    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    // テストケース本体。名前は必ず test から始まる
    // テスト中に異常が発生した場合(=例外がスローされた場合)、テストを Fail とするためその例外をそのまま投げるようにする
    public void testAdd() throws Exception {
        SampleTestTarget1 target = new SampleTestTarget1();

        // 第 1 引数 に期待値、第 2 引数に実際の計算を入れて、等しいかどうか比較する
        assertEquals(2, target.add(1, 1));
        assertEquals(3, target.add(1, 2));
        assertEquals(4, target.add(2, 2));
    }

    public void testDivide() throws Exception {
        SampleTestTarget1 target = new SampleTestTarget1();

        assertEquals(2, target.div(4, 2));
        try {
            target.div(1, 0);
            // 期待した例外が来ない場合、強制的に Fail する
            fail("no argument checking!?");
        } catch (IllegalArgumentException e) {
            // 例外をテストする場合。
            // テストしたい例外のみをキャッチして、それ以外は throws 宣言で Fail 扱いとする
            // テストしたい例外をキャッチした上で、何もしなければ Pass 扱いとなる
        }
    }
}

テストで利用する、検証用のメソッドは以下のようなものがあります。
引数を 2 つとるメソッドは、第 1 引数が期待する値、第 2 引数が実際にテスト対象を呼び出した結果を渡すようにします。

メソッド 用途
assertEquals() 第 1 引数と第 2 引数が等しいかどうか
assertNull() 引数のオブジェクトがnullかどうか
assertNotNull() 引数のオブジェクトがnullでないかどうか
assertTrue() 引数のbooleantrueかどうか
assertFalse() 引数のbooleanfalseかどうか
assertSame() 第 1 引数と第 2 引数が、同じオブジェクトを参照しているかどうか
assertNotSame() 第 1 引数と第 2 引数が、同じオブジェクトを参照していないかどうか

これらは、JUnit3 由来の検証メソッドですが、これら以外にも検証の手段が提供されています。

MoreAsserts | AndroidDevelopers
ViewAsserts | AndroidDevelopers

ProviderTestCase

ContentProviderのためのテストケースです。
テスト用に、モックのContextを利用してテストを行います。

テストを実行すると、テスト用に本体とは分離されたContextのもと、データベース等もすべてテスト向けに特別なものが用意されます。
このため、本体で利用するデータベースへの影響はありません。

ContentProviderのテストでは、queryinsertupdatedeletegetTypeonCreateの動作を保証しておくことが必要です。また、不正なパラメータが与えられた場合などの異常系のテストも必要です。
加えて、ContentProviderを外部向けにエクスポートする場合、アクセスに利用するUriについての正常系と異常系のテストを含めておくことが求められます。

// ProviderTestCase ではなく ProviderTestCase2 を継承すること
public class SampleContentProviderTestCase extends ProviderTestCase2<TestTargetContentProvider> {
    private Context mMockContext;

    public SampleContentProviderTestCase() {
        this(TestTargetContentProvider.class, TestTargetContentProvider.AUTHORITY);
    }

    public SampleContentProviderTestCase(Class<TestTargetContentProvider> providerClass, String providerAuthority) {
        super(providerClass, providerAuthority);
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        mMockContext = getMockContext();
    }

    public void testInsertNewText() throws Exception {
        // モックされたコンテキストから、ContentResolver を取り出し、それを経由してデータベースへとアクセスする
        // テストからアクセスする場合、毎回データベースが作りなおされるため、後始末としてテストで利用したデータを消すなどは必要ない
        // また、データベースそのものも、テスト用のものが作成されるため、本体のデータベースには影響を及ぼさないようになっている
        ContentResolver resolver = mMockContext.getContentResolver();
        ContentValues values = new ContentValues();
        values.put("name", "KeithYokoma");
        Uri newUri = resolver.insert(TestTargetContentProvider.CONTENT_URI, values);
        assertNotNull(newUri);
        assertEquals(ContentUris.withAppendedId(TestTargetContentProvider.CONTENT_URI, 1), newUri);
    }
}

ServiceTestCase

Serviceのためのテストケースです。
Serviceが期待通りのライフサイクルで動作することを保証するためのテストを記述します。

public class SampleServiceTestCase extends ServiceTestCase<TestTargetService> {
    public SampleServiceTestCase() {
        this(TestTargetService.class);
    }

    public SampleServiceTestCase(Class<TestTargetService> serviceClass) {
        super(serviceClass);
    }

    public void testStartingProperly() throws Exception {
        // サービスを問題なく開始できること
        // 何かあれば例外が飛ぶこともチェック
        startService(new Intent(getContext(), TestTargetService.class));
    }

    public void testBinding() throws Exception {
        // 開始するサービスの場合で、バインドをサポートしない場合は、bindService の返り値が null となるので、それをチェック
        IBinder binder = bindService(new Intent(getContext(), TestTargetService.class));
        assertNull(binder);
    }
}

MockContext

Contextをモックするための仕組みとして、MockContextクラスが用意されています。
このクラス自身は、どのメソッドを読んでも、UnsupportedOperationExceptionを投げるだけになっており、自分でモックしたい部分や、モックすべき部分を、MockContextを継承して記述し、各テストケースクラスで、そのContextを注入して使います。

public class SampleTestTarget2 {
    public void startSubActivity(Context context, String hogeExtra) {
        Intent intent = new Intent(context, SubActivity.class);
        intent.putExtra("hoge", hogeExtra);
        context.startActivity(intent);
    }
}
public class SampleTestTarget2TestCase extends AndroidTestCase {
    @Override
    protected void setUp() throws Exception {
        super.setUp();
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    // startSubActivity() を呼び出すテスト
    public void testStartSubActivity() throws Exception {
        SampleTestTarget2 target = new SampleTestTarget2();
        // AndroidTestCase が持っている ActivityContext ではなく、自分でモックした Context
        target.startSubActivity(new SampleTestTarget2Context(getContext()), "hogehoge");
    }

    // Context#startActivity() が、期待通りのコンポーネントに Intent を投げているかテストするための
    // MockContext
    private static class SampleTestTarget2Context extends MockContext {
        private Context mContext;

        public SampleTestTarget2Context(Context baseContext) {
            mContext = baseContext;
        }

        @Override
        public String getPackageName() {
            return mContext.getPackageName();
        }

        @Override
        public void startActivity(Intent intent) {
            // Intent から、Intent の送り先のコンポーネント情報を取り出して、期待値と一致するか確認する
            ComponentName component = intent.getComponent();
            assertEquals(SubActivity.class.getCanonicalName(), component.getClassName());
            // Extra に期待するものが有るか確認する
            assertTrue(intent.hasExtra("hoge"));
            assertEquals("hogehoge", intent.getStringExtra("hoge"));
        }
    }
}

MockContentResolver と MockContentProvider

ContentProviderのデータを元にしたモデルや、Activityのテストを行う際、実際のデータを用いてしまうと、デバイスによってテストがうまくいく場合とうまくいかない場合が出来てしまいます。
これを回避するため、ContentProviderへの問い合わせの際に、直接ContentProviderを使用せず、テスト用に定義したモックのデータを返すようにするものが、MockContentResolverMockContentProviderです。

ContentResolverは、ContentProviderへのアクセスの窓口となるものですので、テスト対象のクラスで、自分で定義したMockContentProviderへアクセスするように設定します。

query()メソッドによるCursorオブジェクトの取得をモックする際には、MatrixCursorクラスを使用します。
MatrixCursorクラスによって、仮想のカラムとデータをCursorオブジェクトに詰め込むことが出来るようになります。

以下に、テスト対象のコードと、それに対応するテストコードを示します。

public class SampleTestTarget3 {
    // DB に問い合わせた結果を、適宜データ構造に当てはめてオブジェクトを作って、リストで返すメソッド
    public List<SampleDBEntity> getAllListFromDB(Context context) {
        List<SampleDBEntity> list = new ArrayList<SampleDBEntity>();

        ContentResolver resolver = context.getContentResolver();
        Cursor c = null;
        try {
            c = resolver.query(TestTargetContentProvider.CONTENT_URI, new String[] { BaseColumns._ID, "name" }, null, null, null);
            // 結果があれば、ポインタを先頭に移動してから処理を始める
            if (c.moveToFirst()) {
                do {
                    // Cursor からデータを取り出してインスタンスを作ってリストに詰める、1 回以上の繰り返し
                    int id = c.getInt(c.getColumnIndex(BaseColumns._ID));
                    String name = c.getString(c.getColumnIndex("name"));
                    list.add(new SampleDBEntity(id, name));
                } while (c.moveToNext());
            }
        } finally {
            // リソースを閉じる
            if (c != null) {
                c.close();
            }
        }
        return list;
    }
}
public class SampleTestTarget3TestCase extends AndroidTestCase {
    public void testGetAllList() throws Exception {
        SampleTestTarget3 target = new SampleTestTarget3();
        List<SampleDBEntity> list = target.getAllListFromDB(new SampleMockContext(getContext()));
        assertNotNull(list);
        MoreAsserts.assertNotEmpty(list);
        assertEquals(3, list.size());

        {
            SampleDBEntity entity = list.get(0);
            assertEquals(1, entity.getId());
            assertEquals("KeithYokoma", entity.getName());
        }

        {
            SampleDBEntity entity = list.get(1);
            assertEquals(2, entity.getId());
            assertEquals("HogeFugao", entity.getName());
        }

        {
            SampleDBEntity entity = list.get(2);
            assertEquals(3, entity.getId());
            assertEquals("HiyoHiyo", entity.getName());
        }
    }

    // モックの本体。ContentProvider そのものをモックしてしまう。
    // モックしたいメソッドを適宜オーバライドすること。
    private static class SampleMockContentProvider extends MockContentProvider {
        @Override
        public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
            // クエリ結果のモック作成
            // MatrixCursor を用いてデータをモックする
            MatrixCursor cursor = new MatrixCursor(projection);
            cursor.addRow(new String[] {"1", "KeithYokoma"});
            cursor.addRow(new String[] {"2", "HogeFugao"});
            cursor.addRow(new String[] {"3", "HiyoHiyo"});
            return cursor;
        }

        @Override
        public void attachInfo(Context context, ProviderInfo info) {
        }
    }

    private static class SampleMockContentResolver extends MockContentResolver {
        public SampleMockContentResolver(Context context) {
            // モックの ContentProvider へアクセスしに行くよう設定する
            ContentProvider provider = new SampleMockContentProvider();

            // AndroidManifest に記述する ContentProvider の宣言をここで動的に行う
            ProviderInfo info = new ProviderInfo();
            info.authority = TestTargetContentProvider.AUTHORITY;
            info.enabled = true;
            info.packageName = TestTargetContentProvider.class.getPackage().getName();

            provider.attachInfo(context, info);

            // ContentProvider を追加する
            addProvider(TestTargetContentProvider.AUTHORITY, provider);
        }
    }

    private static class SampleMockContext extends MockContext {
        private Context mContext;

        public SampleMockContext(Context context) {
            mContext = context;
        }

        // コンテキストをモックして、独自の MockContentResolver を返すようにしてしまう
        @Override
        public ContentResolver getContentResolver() {
            return new SampleMockContentResolver(mContext);
        }
    }
}

このようにすることで、データベースのデータそのものが分離されるので、テストケースでは、テスト対象のビジネスロジックをテストできるようになります。

テストの実行

実行したいテストクラスを選択して、テストを実行します。

右クリックメニューから、Run As > Android JUnit Test を選択すると、自動でターゲットプロジェクトとテストプロジェクトをそれぞれビルドし、ターゲットプロジェクトの apk とテストプロジェクトの apk をインストールし、テストが動き始めます。

テストが動き始めると、順次テスト結果が表示されます。

JUnit Test Result

すべて成功すると、緑色のインディケータが表示されます。
なにか 1 つでも失敗したケースがあると、その時点でインディケータの色が紺色になります。

また、実行したテストケースの一覧も表示されます。
この一覧のなかで、成功・失敗の表示がありますので、どのテストケースが成功・失敗したかが分かるようになっています。

シナリオテスト

ユニットテストでは、各クラス(特にモデルのクラス)が適切な振る舞いをするかどうかをテストしました。
シナリオテストでは、ボタンを押したり、画面を回転した時に、アプリがどのように振る舞うかをテストします。
これを機能テストとも呼んだりしています。

ActivityInstrumentationTestCase

Activity に対して、テストプロジェクトから様々な操作を行うためのテストケースクラスです。

実行すると、実際に実機やエミュレータ上で画面が立ち上がり、自動で UI の操作が行われます。

UI の操作は、立ち上げたActivityのインスタンスを通して、レイアウトのリソースからViewオブジェクトを取り出して操作をするものと、キーイベントのように Instrumentation を通して操作するものがあります。

View のタッチイベントを再現するためのライブラリとして、TouchUtils が用意されています。

// ActivityInstrumentationTestCase2 を継承して、機能テストを書く
public class SampleActivityInstrumentationTestCase extends
        ActivityInstrumentationTestCase2<MainActivity> {
    public SampleActivityInstrumentationTestCase() {
        this(MainActivity.class);
    }

    public SampleActivityInstrumentationTestCase(Class<MainActivity> activityClass) {
        super(activityClass);
    }

    public void testCountUpScenario() throws Exception {
        // getActivity() の呼び出しで、テスト対象の Activity が立ち上がる
        Activity activity = getActivity();

        // UI 操作による View の状態を見るために、View のインスタンスを取り出す
        TextView counter = (TextView) activity.findViewById(R.id.ClickCounter);
        Button button = (Button) activity.findViewById(R.id.CountEventTrigger);

        // 最初は 0
        assertEquals("0", counter.getText().toString());

        // ボタンのクリックをシミュレート
        TouchUtils.clickView(this, button);

        // クリックしたら、カウンタの値がインクリメントされる
        assertEquals("1", counter.getText().toString());
        TouchUtils.clickView(this, button);

        // もう一度クリック
        assertEquals("2", counter.getText().toString());

        // 画面回転する
        activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        Thread.sleep(1000L);

        // クリック回数が引き継がれているはず
        assertEquals("2", counter.getText().toString());
    }

    public void testCallSubActivityAndReturn() throws Exception {
        Activity activity = getActivity();

        // UI 操作による View の状態を見るために、View のインスタンスを取り出す
        TextView counter = (TextView) activity.findViewById(R.id.ClickCounter);
        Button button = (Button) activity.findViewById(R.id.CountEventTrigger);
        Button button2 = (Button) activity.findViewById(R.id.CallSubActivity);

        // 最初は 0
        assertEquals("0", counter.getText().toString());

        // ボタンのクリックをシミュレート
        TouchUtils.clickView(this, button);

        // クリックしたら、カウンタの値がインクリメントされる
        assertEquals("1", counter.getText().toString());

        // Activity の起動を監視する(厳密には Intent を監視する)オブジェクトを作る
        ActivityMonitor monitor = new ActivityMonitor(SubActivity.class.getCanonicalName(), null, true);
        // 監視オブジェクトを登録
        getInstrumentation().addMonitor(monitor);

        // Launch SubActivity をクリック
        TouchUtils.clickView(this, button2);

        // 起動を待つ
        Activity newActivity = getInstrumentation().waitForMonitorWithTimeout(monitor, 3000L);
        // 1 つの Activity が起動しているはず
        assertEquals(1, monitor.getHits());

        // 終わる
        if (newActivity != null)
            newActivity.finish();

        // 戻ってきても状態が復帰できるはず
        assertEquals("1", counter.getText().toString());
    }
}

Robotium

ActivityInstrumentationTestCaseだけでは足りない、より幅広い UI のテストをするためのフレームワークです。

ActivityInstrumentationTestCaseでは、レイアウトの実装を知っている必要がありましたが、Robotium では、この部分もブラックボックス化することができるようになります。
Android ネイティブアプリ版の Selenium とも呼ばれています。

robotium - The world’s leading Android™ test automation framework …

Robotium は、jar 形式のライブラリとなっており、テストプロジェクトにlibsディレクトリを作成して、その中に Robotium の jar を格納することで使用可能となります。

Robotium では、レイアウトの実装をテストから分離するため、Soloと呼ばれる窓口を介して UI を操作します。
本来は UI スレッド上で UI 操作をするための記述をする必要のある部分を、すべてSoloオブジェクトが担ってくれることと、レイアウトの実装を知らなくても、ボタンを押したり、メニューを表示したり、長押ししたりと言ったインタラクションが簡単に記述できることから、ActivityInstrumentationTestCase2よりもブラックボックスなテストが可能です。
また、複数のActivityに渡るシナリオテストも、ActivityMonitorを利用するより非常に簡単に記述できるようになっています。

// Solo クラスを import
import com.jayway.android.robotium.solo.Solo;

// 継承するものは、ActivityInstrumentationTestCase2
public class SampleRobotiumTestCase extends ActivityInstrumentationTestCase2<MainActivity> {
    public SampleRobotiumTestCase() {
        this(MainActivity.class);
    }

    public SampleRobotiumTestCase(Class<MainActivity> activityClass) {
        super(activityClass);
    }

    public void testCountUpScenario() throws Exception {
        Activity activity = getActivity();

        // Robotium ライブラリのコアで、UI の操作の窓口となるオブジェクト
        Solo solo = new Solo(getInstrumentation(), activity);

        // MainActivity が立ち上がってフォアグラウンドにいる
        solo.assertCurrentActivity("MainActivity now.", MainActivity.class);

        // カウンタの数字が 0 であることを確認する
        // 0 と書かれた TextView を画面上から探し出し、あればその TextView オブジェクトを返すメソッド
        assertTrue(solo.searchText("0"));

        // Count up と書かれたボタンをクリックする (View の id を知る必要はない)
        solo.clickOnButton("Count up");

        // 0 と書かれた TextView はなくなり、1 と書かれたTextView になるはず
        assertFalse(solo.searchText("0"));
        assertTrue(solo.searchText("1"));

        solo.clickOnButton("Count up");

        assertFalse(solo.searchText("1"));
        assertTrue(solo.searchText("2"));

        // 横画面に回転する
        solo.setActivityOrientation(Solo.LANDSCAPE);

        // 状態が保存され、2 と書かれた TextView が引き続き居るはず
        assertTrue(solo.searchText("2"));
    }

    public void testCallSubActivityAndReturn() throws Exception {
        Activity activity = getActivity();

        Solo solo = new Solo(getInstrumentation(), activity);

        // MainActivity が立ち上がってフォアグラウンドにいる
        solo.assertCurrentActivity("MainActivity now.", MainActivity.class);

        // カウンタの数字が 0 であることを確認する
        // 0 と書かれた TextView を画面上から探し出し、あればその TextView オブジェクトを返すメソッド
        assertTrue(solo.searchText("0"));

        // Count up と書かれたボタンをクリックする (View の id を知る必要はない)
        solo.clickOnButton("Count up");

        // 0 と書かれた TextView はなくなり、1 と書かれたTextView になるはず
        assertFalse(solo.searchText("0"));
        assertTrue(solo.searchText("1"));

        // Launch SubActivity ボタンを押す
        solo.clickOnButton("Launch SubActivity");

        // SubActivity が起動し、フォアグラウンドに居るはず
        solo.assertCurrentActivity("SubActivity now.", SubActivity.class);

        // メニューキーを押した
        solo.sendKey(KeyEvent.KEYCODE_MENU);
        // ActionBarを使っているなら、以下でも良い
        // solo.clickOnActionBarItem(0);

        // 戻る
        solo.getCurrentActivity().finish();

        // 状態復帰できているはず
        assertTrue(solo.searchText("1"));
    }
}

UIAutomator

ADT リビジョン 21 以降から導入され、Android 4.1 以降で動作するように作られた、UI のテストフレームワークです。
Android SDK に含まれています。

プロジェクトのセットアップ

UIAutomator を使ったプロジェクトの作成は、Android 用プロジェクトではなく、Java 用プロジェクトとして作成します。

New Java Project Wizard

次に、テストプロジェクトで利用するライブラリのセットアップを行います。

Library Configuration

Libraries タブから、Add Libraries… を選択し、表示されたダイアログから、JUnit を選択します。

Add JUnit

バージョンを聞かれるので、JUnit3 を選択します。

Select JUnit Version

次に、Add External Jars… から、android.jar と uiautomator.jar をピックアップします。
この 2 つは、SDK のディレクトリ以下のplatforms/android-16/またはplatforms/android-17ディレクトリに配置されています。

External Jars

選択したら、このような画面になるはずです。

Configuration Done

次に、コマンドラインから、UIAutomator のプロジェクトディレクトリへ移動し、下記のコマンドを実行します。

$ android create uitest-project -p UIAutomatorSample -n UIAutomatorSample -t 21
Added file UIAutomatorSample/build.xml

以上で、プロジェクトのセットアップは終了です。

テストの作成

UiAutomatorTestCaseを継承して、テストケースクラスを作ります。
JUnit3 をベースとするため、テストケースのメソッド定義は他のテストケースと同様な命名規則に従います。

// 端末ホーム画面からアプリを起動して操作するテスト
public class MainActivityAutomatorTestCase extends UiAutomatorTestCase {
    public void testMainActivityButtonClick() throws Exception {
        // デバイスオブジェクトの取得。このオブジェクトを介して、デバイスの状態を取得したり、UI の操作を行ったりする。
        UiDevice device = getUiDevice();

        // ホームボタンを押す
        device.pressHome();

        // ホームボタンに有るターゲットのアイコンをタップする
        UiObject launchIcon = new UiObject(new UiSelector().textContains("TestTarget"));
        launchIcon.clickAndWaitForNewWindow();

        // 起動した(指定したパッケージ名のアプリがフォアグラウンドに居て、オブジェクトの取得が無事に出来る)
        UiObject app = new UiObject(new UiSelector().packageName("jp.mixi.sample.test"));
        assertTrue(app.exists());

        device.pressBack();

        // もう一度タップ
        UiObject launchIcon2 = new UiObject(new UiSelector().textContains("TestTarget"));
        launchIcon2.clickAndWaitForNewWindow();

        // カウンターの初期値は 0
        UiObject firstCounterState = new UiObject(new UiSelector().text("0"));
        assertTrue(firstCounterState.exists());

        // カウントアップ
        UiObject countUp = new UiObject(new UiSelector().text("Count up"));
        countUp.click();

        // カウンターが更新される
        UiObject secondCounterState = new UiObject(new UiSelector().text("1"));
        assertTrue(secondCounterState.exists());
    }
}

テストの実行

テストプロジェクトの中で、下記のコマンドを実行します。

$ ant build
Buildfile: /Path/To/UIAutomatorTestProject/build.xml

-check-env:
 [checkenv] Android SDK Tools Revision 21.1.0
 [checkenv] Installed at /Path/To/android-sdk-macosx

-build-setup:
     [echo] Resolving Build Target for UIAutomatorTestProject...
[getuitarget] Project Target:   Android 4.2.2
[getuitarget] API level:        17
     [echo] ----------
     [echo] Creating output directories if needed...
    [mkdir] Created dir: /Path/To/UIAutomatorTestProject/bin/classes

-pre-compile:

compile:
    [javac] Compiling 1 source file to /Path/To/UIAutomatorTestProject/bin/classes

-post-compile:

-dex:
      [dex] input: /Path/To/UIAutomatorTestProject/bin/classes
      [dex] Converting compiled files and external libraries into /Path/To/UIAutomatorTestProject/bin/classes.dex...

-post-dex:

-jar:
      [jar] Building jar: /Path/To/UIAutomatorTestProject/bin/UIAutomatorTestProject.jar

-post-jar:

build:

BUILD SUCCESSFUL
Total time: 2 seconds

コマンドを実行すると、プロジェクトディレクトリ以下のbin/ディレクトリの中に、jar ファイルが生成されます。

次に、この jar ファイルをデバイスへ転送し、デバイス上で jar を実行します。

$ adb push UIAutomatorTestProject.jar /data/local/tmp
$ adb shell uiautomator runtest UIAutomatorTestProject.jar -c jp.mixi.sample.test.automator.MainActivityAutomatorTestCase

すると、画面がひとりでに動き始め、コンソールにテスト結果が出力されるようになります。

INSTRUMENTATION_STATUS: current=1
INSTRUMENTATION_STATUS: id=UiAutomatorTestRunner
INSTRUMENTATION_STATUS: class=jp.mixi.sample.test.automator.MainActivityAutomatorTestCase
INSTRUMENTATION_STATUS: stream=
jp.mixi.sample.test.automator.MainActivityAutomatorTestCase:
INSTRUMENTATION_STATUS: numtests=1
INSTRUMENTATION_STATUS: test=testMainActivityButtonClick
INSTRUMENTATION_STATUS_CODE: 1
INSTRUMENTATION_STATUS: current=1
INSTRUMENTATION_STATUS: id=UiAutomatorTestRunner
INSTRUMENTATION_STATUS: class=jp.mixi.sample.test.automator.MainActivityAutomatorTestCase
INSTRUMENTATION_STATUS: stream=.
INSTRUMENTATION_STATUS: numtests=1
INSTRUMENTATION_STATUS: test=testMainActivityButtonClick
INSTRUMENTATION_STATUS_CODE: 0
INSTRUMENTATION_STATUS: stream=
Test results for WatcherResultPrinter=.
Time: 5.77

OK (1 test)


INSTRUMENTATION_STATUS_CODE: -1

通常、ActivityInstrumentationTestCase等では、アプリの署名が合致しないものはテストができませんでしたが、UIAutomator では、署名に関係なく UI の操作をテストすることができます。
これにより、例えば、他のアプリとの連携をする部分のテストも容易に記述できるようになります。

画面から UI の要素を取り出して操作するには、画面に表示されているレイアウトの階層構造をたどることの他、サンプルのように、表示されている文字列でたどったり、あるいは、View のクラス名 (FQDN) からたどったりすることで、要素を取り出して操作をすることが出来るようになっています。
ImageView など文字列でたどれないものは、android:contentDescription属性を元に辿る手段もあります。

デバイス上で表示されている画面のレイアウト構造を見るための手段として、uiautomatorviewerというコマンドも用意されています。