Tech Racho エンジニアの「?」を「!」に。
  • 開発

Robotiumを使ってAndroidの自動化テストを試してみた

ちょっと前の話になりますが、今後Android/iOSのテストを自動化していきたいという話が社内の会議で出たので調査を行いました。

まだプロジェクトでの運用が軌道に乗ったとは言えない状況ですが、使えそうだという手応えは得られました。
その時に調べた結果について、Androidのみとなりますが書いてみたいと思います。

まず、テストを自動化するにあたりどのテストツールを使用するかの検討を行いました。
その中でAppiumMonkeyTalkCalabashEspressoRobolectricなどの名前が出てきましたが、APIレベル1.6以上で使用可能と制限が少なめで情報も比較的多く流れているRobotiumを採用しました。

公式サイト: https://code.google.com/p/robotium/
GitHub: https://github.com/RobotiumTech/robotium/graphs/commit-activity

Robotiumの導入方法

導入は簡単なのでほとんど詰まるところは無いと思いますが、手順を載せておきます。

※この情報は2014/01時点のものになります
※Eclipse用語は日本語化されてます

  1. Eclipseの新規プロジェクトから「Android テスト・プロジェクト」を選択して次へを押下
  2. Select Test Target画面で「An existing Android project:」を選択してからテスト対象プロジェクトを選択して次へを押下
  3. ビルドターゲットを選択して完了
  4. パッケージ・エクスプローラーから作成されたプロジェクトを右クリックし、プロパティを開く
  5. Javaのビルドパスのライブラリータブで公式サイトからダウンロードしたrobotiumのjarファイルを追加する(今回はrobotium-solo-5.0.1.jarを追加、バージョンによって名称は変化)
    ※libsディレクトリを作成し、その中にjarを入れるだけでも良いはずですが、private librariesに対するjavadocの添付がうまくいかなかったのでこの方法を取っています
    (Javadocの設定はライブラリータブに追加したjarのJavadocロケーションを選択して編集を押し、公式サイトからダウンロードしたrobotium-solo-5.0.1-javadoc.jarを指定することで可能)
  6. Javaのビルドパスの順序およびエクスポートタブで先ほど追加したRobotiumのjarファイルにチェックを入れる(これをしないとSDK17以上の場合にNoClassDefFoundErrorが発生する)
  7. ActivityInstrumentationTestCase2<対象Activity>を継承したクラスを作成してJUitテストコードを記述する(詳しくはrobotiumの公式サイトからダウンロードできるExampleやJUnitの説明を参照)
  8. パッケージ・エクスプローラーで対象のプロジェクトを右クリックし、実行-Android JUnit Testを開く

コマンドラインでの実行方法

コマンドラインからテストコードを実行する場合は以下のコマンドで出来ます。(日本語で書いてある箇所は可変です)
※ブラウザ上で改行が入っているかもしれませんが、各コマンドは1行で実行して下さい

  • 全てのテストケースを実行する

    adb -s 端末ID shell am instrument -w パッケージ名/android.test.InstrumentationTestRunner

  • 特定のテストクラスのみ実行する

    adb -s 端末ID shell am instrument -w -e class クラス名 パッケージ名/android.test.InstrumentationTestRunner

  • 特定のメソッドのみ実行する

    adb -s 端末ID shell am instrument -w -e class クラス名\#メソッド名 パッケージ名/android.test.InstrumentationTestRunner

テストコードを書いて動かしてみる

弊社では超画像という電子書籍ビューアと一緒に、書籍を管理するための本棚アプリも開発しており、今回はその本棚アプリを題材にしたいと思います。まず以下のような機能を持つ本棚アプリがあると仮定します。(説明のために余計な機能を省いて記載しています、もう本棚の面影がないですが……)

  • 本棚画面の同期ボタンを押すと購入情報などの同期が始まる。未ログイン状態の場合はログインダイアログが表示される。
  • ログインダイアログではIDとパスワードを入力することでログインが可能。その他IDやパスワードの保存・非保存設定などの機能が付いている
  • ログインが成功すると同期のプログレスダイアログが表示され、同期が完了すると。完了メッセージがToastで表示される。
  • 本棚画面の設定ボタンを押すと設定画面が表示される。ログイン中の場合は設定画面にログアウトボタンが表示される。(未ログイン状態の場合はログインボタンが表示される)

図1

ここでは以下のような点をテストするためのサンプルコードを載せます。(例のため、細かい限界値チェックなどは省いています)

  • 正しくActivity、ダイアログ、Toastが表示されているか
  • ログインダイアログの各種機能が正しく動くか
  • ログイン、同期ができるか(今回は簡易的に、同期が成功した時のみ表示されるトーストが表示されたかで判断しています)
  • ログアウトできるか
public class ShelfTest extends ActivityInstrumentationTestCase2<ShelfActivity> {
    private static final String OK = "OK";
    private static final String CANCEL = "キャンセル";
    private Solo solo;
    private Context con;

    public ShelfTest() {
        super(ShelfActivity.class);
    }

    @Override
    protected void setUp() throws Exception {
        solo = new Solo(getInstrumentation(), getActivity());
    }

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

    public void testLogin() throws Exception {
	    assertShelfActivity()

        // ログインダイアログ未入力チェック
        syncronize("", null, true, "", null, true, OK);
        assertLoginIdPassEmptyError();
        syncronize("", "", true, "a", "", true, OK);
        assertLoginIdPassEmptyError();
        syncronize("a", "", true, "", "a", true, OK);
        assertLoginIdPassEmptyError();

        // ログイン失敗チェック
        syncronize("正しいログインID", "a", true, "間違ったパスワード", "", true, OK);
        assertLoginIdPassError();

        // ログイン成功チェック
        syncronize("正しいログインID", "正しいログインID", true, "正しいパスワード", "間違ったパスワード", true, OK);
        assertSyncronizeSuccess();

        // ログアウト
        logout();

        // IDとパスの保存チェック片方ずつ外して再チェック
        syncronize("aaaaa", "正しいログインID", true, "bbbbb", "正しいパスワード", false, OK);
        assertLoginIdPassError();
        syncronize("abcde", "aaaaa", false, "bbbbb", "", true, OK);
        assertLoginIdPassError();

        // キャンセルボタンチェック
        syncronize("abcde", "", false, "", "bbbbb", false, CANCEL);
        syncronize("aaaaa", "", true, "ccccc", "bbbbb", true, CANCEL);
    }

    private void logout() {
        // 本棚画面のフッタにある設定ボタンを押下
        solo.clickOnView(solo.getView(R.id.footer_preferences_button));
        assertPreferenceActivity();
        // 設定画面内にあるログアウトボタンを押下
        solo.clickOnText(con.getString(R.string.title_logout));
        solo.goBackToActivity("ShelfActivity");
        assertShelfActivity();
    }

    private void syncronize(String id, String oldId, boolean idCheck, String pass, String oldPass, boolean passCheck, String clickButtonText) {
        // 本棚画面のフッタにある同期ボタンを押下
        solo.clickOnView(solo.getView(R.id.footer_synchronize_button));
        assertDialogOpen();
        // ログインダイアログに表示されたテキスト入力欄からIDとパスワードを取得
        EditText loginEdit = solo.getEditText(0);
        EditText passEdit = solo.getEditText(1);

        if (oldId != null) {
            assertTrue("保存ユーザーIDが不正", oldId.equals(loginEdit.getText().toString()));
        }
        if (oldPass != null) {
            assertTrue("保存パスワードが不正", oldPass.equals(passEdit.getText().toString()));
        }
        // 一旦入力テキストを消してから新しいIDとパスワードを入力
        solo.clearEditText(loginEdit);
        solo.clearEditText(passEdit);
        solo.typeText(loginEdit, id);
        solo.typeText(passEdit, pass);

        // IDやパスワードを保存する、のチェックボックスの設定が変更されていたらチェックを切り替える
        if (solo.isCheckBoxChecked(0) != idCheck) {
            solo.clickOnCheckBox(0);
        }
        if (solo.isCheckBoxChecked(1) != passCheck) {
            solo.clickOnCheckBox(1);
        }
        solo.clickOnButton(clickButtonText);
    }

    private void assertSyncronizeSuccess() {
        // 同期中ダイアログが正しく表示されるかチェック
        assertTrue("同期中ダイアログが表示されない", waitForText(solo, con.getString(R.string.sync_dlg_title), 3));
        // 同期完了時のトーストが正しく表示されるかチェック
        assertTrue("同期完了トーストが表示されない", waitForText(solo, con.getString(R.string.sync_mes_success), 5));
    }

    private void assertLoginIdPassEmptyError() {
        // ログイン失敗ダイアログが正しく表示されるかチェック、表示後OKボタン押下
        assertTrue("ID、パスワード未入力エラーにならない", waitForText(solo, con.getString(R.string.login_id_empty), 3));
        solo.clickOnButton(OK);
    }

    private void assertLoginIdPassError() {
        // ログイン失敗ダイアログが正しく表示されるかチェック、表示後OKボタン押下
        assertTrue("ID、パスワード間違いエラーにならない", waitForText(solo, con.getString(R.string.login_idpw_error), 3));
        solo.clickOnButton(OK);
    }

    private void assertShelfActivity() {
        assertTrue("本棚画面が表示されていません", waitForActivity(solo, ShelfActivity.class, 5));
    }

    private void assertPreferenceActivity() {
        assertTrue("設定画面が表示されていません", waitForActivity(solo, ShelfPreferenceActivity.class, 5));
    }

    private void assertDialogOpen() {
        assertTrue("ダイアログが表示されません", solo.waitForDialogToOpen(3000));
    }

    private static boolean waitForText(Solo solo, String text, int second) {
        return solo.waitForText(text, 1, second * 1000);
    }

    private static boolean waitForActivity(Solo solo, Class<? extends Activity> clazz, int second) {
        return solo.waitForActivity(clazz, second * 1000);
    }
}

上記のコードを簡単に説明すると、リソースIDを使用して各種ボタンを押し、
その結果正しいActivity・ダイアログ・Toastなどが表示されているかを随時確認しています。

できるだけコメントを入れたつもりなので、詳細は上記コードと公式サイトのAPI仕様(主にSoloクラス)を読んでいただけると助かります。

今回は全部自動で確認を行っていますが、画像ビューアなど、人間の目で見て確認する必要があるテストも存在します。そういう場合はSolo.takeScreenshotメソッドなどでスクリーンショットが取れるので、自動テスト終了後に出力されたスクリーンショットを人間が確認して問題が無ければテスト完了とする、などの運用も可能です。

事前に正しい結果のスクリーンショットを撮っておき、自動テスト内で撮ったスクリーンショットと事前に保存しておいたスクリーンショットを比較するなどの方法を取れば画像ビューアのテスト自動化も夢ではありません。(さすがに全件は難しいですが……)

この辺り、今後も研究していきたいところです。


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。