Androidの自動テストツール、今(2013年)から使うなら何がよいのか

あらすじ

Androidのテストを自動化したいので、テストツールの選定をしてみたが、昔の記事がヒットする事が多く、何を使えばいいのかよくわからん。

とはいっても、明確に「どんなテストがしたい」という方針もなく、とっかかりとしてどんなツールがあってどのくらい盛り上がってるのかが知りたかった。

環境

  • Windows 7
  • AndroidDeveloperTools Build: v21.1.0-569685

とりあえず Win メインで。

とっかかり

ロジックまわりのテスト

ロジック的なものは、 JUnit 拡張の TestCase クラスを使えば何とか書けそうというのはわかった。

2011 年の記事だけど、 JUnit で書くという大前提は崩れていないはず…。

画面遷移やGUIまわりのテスト

困ったのはこっち。どう書けばいいんだろう。

「Android 自動テスト ツール」 とかで調べてみた結果、以下のような記事が引っ掛かった。

これも 2011 年の記事だが、この中では NativeDriver , robotium , Scirocco の 3 ツールでは最終的に NativeDriver に集約されていくだろうという結論になっている。

…が、 2013 年現在においても本当にそうなのか?

結論

2013 年 08 月の時点ではこんな感じ。

ツール ソース Android iOS テスト記述可能な言語 備考
NativeDriver svn - - - 開発終了
robotium GitHub 1.6 以上 Java
Scirocco GitHub - - - 2012/09 で更新が止まっている
Monkeyrunner sdk内 Java Python Jython で実行 / Plugin は Java で書ける / 今回はうまく動かせなかった
Appium GitHub 4.2 以上 Node.js Python PHP Ruby Java Windows 版は beta / iOS がメイン? ファイル置場
Spoon GitHub 4.1 以上 Java Maven 実行推奨?
Selenroid GitHub Java Windows 版は現状未サポート / Ruby でもテスト書ける?
uiautomator sdk内 4.1 以上 Java 今回はうまく動かせなかった

Android / iOS 欄の は公式で動くと謳っているが、詳しいバージョンまで見つけられなかったものに記入。

  • 今でもよく検索に引っかかる NativeDriver はとっくに開発終了されている
  • 新鋭( 2013 ~)ツールは便利な機能も多い印象だが、 Windows に未サポートのものが多い(バグ踏んでも泣かない)
  • Windows 使いなら robotium あたりに行くのが良い?

以下、ひとつずつ見てみる。

テストツール

NativeDriver - 開発終了

Selenium WebDriver の源流になっている WebDriver (Google謹製 / 2009年) の流れをくんでおり、かつAndroid, iOS対応とし、現在の主流なのかと思ったが、2011年で更新が止まっている?

NativeDriver はすでに 開発終了 しており、一部 Selenium 2(WebDriver 統合)に還元されて天に召された模様。

上記のフォーラムで、 NativeDriver ユーザはどうすればよいか今後の方針が述べられている。

  1. Android Instrumentation のような公式ツールを使え
  2. robotium のようなサードパーティツールを使え
  3. ( NativeDriver を引き続き使うなら)自分自身で Hack し、より良くしてみろ

ということらしい。というわけで、2013年の時点では候補から除外した方がよさげ。

(ワードにもよるが) ググると高確率でこれが上位にあがってくるので、バリバリ使われているのかと思ったが、正式に凍結を声明した記述が見つかったので安心。

robotium

この中では一番プロジェクトの歴史が古く、2013年に入っても精力的にコミットなされている。

This project is neither affiliated with Google nor with OpenQA (Selenium).

「Google プロジェクトでも Selenium プロジェクトでもない」という事で、非公式 Selenium 的な感じの様子。

  • Android 1.6 以上をサポート
  • apk ファイルのみでもテスト可
  • プリインストールされた端末でもテスト可
  • ハイブリッドアプリもテスト可能( robotium 4.0 から)

これはちょっと Hello World してみよう。

前準備

公式ページより Jar File をダウンロード。現在の最新版は robotium-solo-4.2.jar

これを TESTPROJECT/libs/ に入れる。 libs ディレクトリに入れると、 Eclipse の Package Explorer で見たときに Android Dependencies 下に robotium の jar が見えるはず。

見えなければプロパティから追加。 testing - Android Robotium NoClassDefFoundError - Stack Overflow

シナリオ作成

基本的には JUnit のテストケース作成と同じ要領で進む。

適当なログイン画面のログインボタン押して、戻るだけのテストケース LoginActivityTest.java

// robotium インポート
import com.jayway.android.robotium.solo.Solo;

()

// 既存のJUnitテストクラス
public class LoginActivityTest
  extends ActivityInstrumentationTestCase2<LoginActivity> {

()
    // に robotium を使ったテストを追加
    public void testMove() throws Exception {
            Solo solo = new Solo(getInstrumentation(), getActivity());
            // 座標指定してクリック
            solo.clickOnScreen(200, 600);
            // 画面から引数に指定したテキストを見つけて?クリック
            solo.clickOnText("Hoge");
            // ログインと書かれたボタンを見つけてクリック
            solo.clickOnButton("ログイン");

            solo.assertCurrentActivity("次の画面へ", MainActivity.class);
            
            // "/sdcard/Robotium-Screenshots/" にスクリーンショット保存
            solo.takeScreenshot();
            // アクティビティ戻る
            solo.goBack();
            
            solo.assertCurrentActivity("戻ってきた", LoginActivity.class);
        }
()

solo.clickOnButton("ログイン") でボタンを一気に押してくれるのが非常に便利に感じる。

(ボタンの取得の仕方とかは、今のところ findViewById でとってくる Button button = (Button) activity.findViewById(com.example.testapp.R.id.login_button) 方法しかしらないので)

また、スクリーンショット機能なども実装されており、メソッドを呼び出すだけでSDカードに入れてくれる!便利!

Scirocco - 現Scirocco for WebDriver

※ Google Project Hosting 版は Development Discontinued とされており、今はソニックスが管理しているみたい。また、旧版は Scirocco , ソニックス版は Scirocco WebDriver となっておりアーキテクチャが変わっているらしい。

robotium, scirocco plug-in , scirocco TestManagementSystem から成り立つテストツール。

基本的には robotium に機能がプラスされたツールなんだろう。テストのレポートやスクリーンショットがとれる模様。(前述のとおり、 スクリーンショットは robotium でも(今は?)できる様子)

けど、 robotium が猛烈に更新されている一方で、こっちは更新が止まっている(遅れている?)ようなので、 Scirocco は深追いせずこれで終わり。

MonkeyRunner

これだけ他のツールとちょっと毛色が違う感じがする。

Pythonで書ける、画面のボタン選択などは 座標指定 。(座標を調べる事自体もめんどいし、複数端末あると端末分スクリプト作らなきゃいけない?)

画面のいろいろなところをやみくもにぽちぽちするテストもできる。

ちょっと使ってみよう。…と思ったけどなんかダメだった。

前準備

まず、 SDK_ROOT\sdk\tools\monkeyrunner.bat を実行してみる。 Jython で実行するようなので Java (は入っていると思うが) と Python が必要?

$ monkeyrunner.bat
Jython 2.5.0 (Release_2_5_0:6476, Jun 16 2009, 13:33:26)
[Java HotSpot(TM) Client VM (Oracle Corporation)] on java1.7.0_17
>>>

シナリオ作成

上記の Simple monkeyrunner Program をそのまま流し込んでみようと思ったんだけど、2行目でエラー…。

$ monkeyrunner.bat
Jython 2.5.0 (Release_2_5_0:6476, Jun 16 2009, 13:33:26)
[Java HotSpot(TM) Client VM (Oracle Corporation)] on java1.7.0_17
>>> from com.android.monkeyrunner import MonkeyRunner, MonkeyDevice
>>> device = MonkeyRunner.waitForConnection()
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] Adb rejected adb port forwarding command: cannot bind socket
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice]com.android.ddmlib.AdbCommandRejectedException: cannot bind socket
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.ddmlib.AdbHelper.createForward(AdbHelper.java:545)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.ddmlib.Device.createForward(Device.java:481)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.chimpchat.adb.AdbChimpDevice.createManager(AdbChimpDevice.java:126)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.chimpchat.adb.AdbChimpDevice.<init>(AdbChimpDevice.java:72)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.chimpchat.adb.AdbBackend.waitForConnection(AdbBackend.java:122)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.chimpchat.ChimpChat.waitForConnection(ChimpChat.java:91)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.monkeyrunner.MonkeyRunner.waitForConnection(MonkeyRunner.java:75)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at java.lang.reflect.Method.invoke(Method.java:601)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:175)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.core.PyReflectedFunction.__call__(PyReflectedFunction.java:190)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.core.PyObject.__call__(PyObject.java:381)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.core.PyObject.__call__(PyObject.java:385)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.pycode._pyx2.f$0(<stdin>:1)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.pycode._pyx2.call_function(<stdin>)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.core.PyTableCode.call(PyTableCode.java:165)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.core.PyCode.call(PyCode.java:18)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.core.Py.runCode(Py.java:1197)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.core.Py.exec(Py.java:1241)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.util.PythonInterpreter.exec(PythonInterpreter.java:147)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.util.InteractiveInterpreter.runcode(InteractiveInterpreter.java:89)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.util.InteractiveInterpreter.runsource(InteractiveInterpreter.java:70)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.util.InteractiveInterpreter.runsource(InteractiveInterpreter.java:46)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.util.InteractiveConsole.push(InteractiveConsole.java:110)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.util.InteractiveConsole.interact(InteractiveConsole.java:90)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at org.python.util.InteractiveConsole.interact(InteractiveConsole.java:60)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.monkeyrunner.ScriptRunner.console(ScriptRunner.java:193)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.monkeyrunner.MonkeyRunnerStarter.run(MonkeyRunnerStarter.java:73)
130828 20:34:40.534:S [main] [com.android.chimpchat.adb.AdbChimpDevice] at com.android.monkeyrunner.MonkeyRunnerStarter.main(MonkeyRunnerStarter.java:189)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
          at com.google.common.base.Preconditions.checkNotNull(Preconditions.java:191)
          at com.android.chimpchat.adb.AdbChimpDevice.<init>(AdbChimpDevice.java:74)
          at com.android.chimpchat.adb.AdbBackend.waitForConnection(AdbBackend.java:122)
          at com.android.chimpchat.ChimpChat.waitForConnection(ChimpChat.java:91)
          at com.android.monkeyrunner.MonkeyRunner.waitForConnection(MonkeyRunner.java:75)
          at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
          at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
          at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
          at java.lang.reflect.Method.invoke(Method.java:601)

java.lang.NullPointerException: java.lang.NullPointerException

adb kill-server してみては?という記事は見つけたけど特に変わらず…。

Appium

Selenium ライクで iOS, Android 両方 のテストを作成できる。

またテストコードは Node.js, Python, PHP, Ruby, Java などで書けるようだ。

ただし、以下のような依存がある。

Requirements

General:

  • Mac OS X 10.7 or higher, 10.8 recommended (Linux OK for Android-only; support for Windows is in “beta”)
  • Node and npm (brew install node) (Node must be >= v0.8)

For iOS automation:

  • XCode
  • Apple Developer Tools (iPhone simulator SDK, command line tools)

For Android automation:

  • Android SDK API >= 17

今のところ Windows は beta 版 のようなので、今回は試用見送り。あと Android API 17 以上(= 4.2以上) というのも意外とハードルが高い。

iOSがメインで、Androidもテストできますよ、的な感じなのかも。

Spoon

Android 4.1 以上必須、実行に maven 推奨。

日本語の情報は少ない。

Selendroid

Selenium for Android Apps という事で Android ネイティブアプリや Web ビューのテストを Selenium で書ける?

Mac か Linux で動作確認。 Windows 版は not offially supported でいくつか問題あり。

日本語の情報は少ない。

uiautomator

はてブコメントや Twiter にて言及いただいたので調査。抜けていたのは、単純に知らなかったからです…。

Monkeyrunner と同じ場所に入っていた。

  • AndroidSDK 21 でサポートされた
  • Android 4.1 以降で動作?(サポートされているのが 4.1 以降?)
  • これは adb shell からたたく感じのツールみたい

ちょっとさわってみようと思ったが、これもかなり苦戦する。挙句動かせないという。

AndroidSDK に同梱されているツールは一筋縄で動かないなぁ。

前準備

  • テスト用プロジェクト右クリックし、 Properties -> Java Build Path -> Add External JARs から SDK_ROOT\sdk\platforms\android-17\uiautomator.jar を追加する
  • テストクラスを UIAutomatorTestCase で extend する

シナリオ作成

UIxxx クラスで端末を操作していく。上記のサイトを参考に…。

public class LoginActivityUITest extends UiAutomatorTestCase {
  public void testHelloWorld() throws Exception {
    getUiDevice().pressHome();

    UiObject allAppsButton = new UiObject(new UiSelector().description("アプリ"));
    allAppsButton.clickAndWaitForNewWindow();
    UiObject appsTab = new UiObject(new UiSelector().text("アプリ"));
    appsTab.click();
  }
}

実行

ただ JUnit 実行すればいいってわけじゃないらしい。結構めんどい。

ビルドする

SDK_ROOT\sdk\tools\ 下にある android.bat を使ってビルド。

$ android.bat create uitest-project -n hoge -t x -p .

それぞれの引数はこう。

Options:
  -p --path    : The new project's directory. [required]
  -n --name    : Project name.
  -t --target  : Target ID of the new project. [required]

--targetandroid.bat list コマンドで取得できる。(ずっと API レベルのことだと思って 17 とか指定していたら Error: Target id is not valid. Use 'android.bat list targets' to get the target ids. エラーで死んでた)

$ android list
Available Android targets:
----------
(略)
----------
id: 6 or "android-17"
     Name: Android 4.2.2
     Type: Platform
     API level: 17
     Revision: 2
     Skins: HVGA, QVGA, WQVGA400, WQVGA432, WSVGA, WVGA800 (default), WVGA854, WXGA720, WXGA800, WXGA800-7in
     ABIs : armeabi-v7a
Available Android Virtual Devices:
    Name: test
    Path: C:\USER_PROFILE\.android\avd\test.avd
  Target: Android 3.2 (API level 13)
     ABI: armeabi
    Skin: 480x854
Snapshot: true

実行すると build.xml ができる。

$ android.bat create uitest-project -n hoge -t 6 -p C:\hoge
Added file C:\hoge\build.xml

ant build して bin 下にできる jar ファイルを回収する

$ C:\apache-ant-1.9.2\bin\ant build
Buildfile: C:\hoge\build.xml

-check-env:
 [checkenv] Android SDK Tools Revision 21.1.0
 [checkenv] Installed at SDK_ROOT\sdk

-build-setup:
     [echo] Resolving Build Target for hoge...
[getuitarget] Project Target:   Android 4.2.2
[getuitarget] API level:        17
     [echo] ----------
     [echo] Creating output directories if needed...

-pre-compile:

compile:

-post-compile:

-dex:
      [dex] input: C:\hoge\bin\classes
      [dex] Converting compiled files and external libraries into C:\hoge\bin\classes.dex...

-post-dex:

-jar:
      [jar] Building jar: C:\hoge\bin\hoge.jar

-post-jar:

build:

BUILD SUCCESSFUL
Total time: 4 seconds
転送してテスト実行

次は adb コマンドで 今作った jar ファイルを端末に転送する。

$ pwd
SDK_ROOT\sdk\platform-tools
$ adb.exe push C:\hoge\hoge.jar /data/local/tmp
463 KB/s (4271 bytes in 0.009s)

そして実行…だけど permission denied ??

$ adb.exe shell uiautomator runtest hoge.jar -c com.example.test
uiautomator: permission denied

権限とかも、特に間違ってないみたいなんだけど…。

$ ls -l /data/local/tmp/*.jar
ls -l /data/local/tmp/*.jar
-rw-rw-rw- shell    shell        4271 2013-09-02 20:58 hoge.jar

とりあえず、用意が相当めんどくさいということはわかった。

それを補って余りある API が提供されているのだろうか…。

おまけ

Android API レベルとOSとリリース日の対応のメモ。裏をとるのがめんどいのでおまけなので、 Wikipedia 情報を全面的に信頼する。

OS API レベル コードネーム リリース
Android 4.3 18 JellyBean 2013/07/24
Android 4.2 17 JellyBean 2012/11/13
Android 4.1 16 JellyBean 2012/06/27
Android 4.0.3 - 4.0.4 15 IceCreamSandwich 2012/03/28(4.0.4)
Android 4.0 - 4.0.2 14 IceCreamSandwich 2011/10/18
Android 3.2 13 Honeycomb 2011/07/15
Android 3.1 12 Honeycomb 2011/05/10
Android 3.0 11 Honeycomb 2011/02/22
Android 2.3.3 - 2.3.7 10 Gingerbread 2011/09/20(2.3.7)
Android 2.3 - 2.3.2 9 Gingerbread 2010/12/06(2.3)
Android 2.2 8 Froyo 2010/05/21
Android 2.1 7 Eclair 2010/01/12
Android 2.0.1 6 Eclair 2009/12/03
Android 2.0 5 Eclair 2009/10/26
Android 1.6 4 Donut 2009/09/15
Android 1.5 3 Cupcake 2009/04/30
Android 1.1 2 - 2009/02/09
Android 1.0 1 - 2008/09/23

関連記事(この記事の初版より古い記事はリンクがグレーで表示されます)

  1. 2013/08/04 [Java] [Android] Androidアプリのインストール、起動方法によってIntentのタイプが微妙に変わる件
  2. 2019/07/09 [Android] Android Studioのコードフォーマットをプロジェクトで統一する
  3. 2017/05/31 [Android] Android StudioでAndroid Lint
  4. 2017/02/28 [Android] [Gradle] GradleのresValueで値をリソースに設定する
  5. 2016/09/30 [Android] Androidのapkの総メソッド数を調べてMulti-dexを導入するか否かを判断する
  6. 2016/05/29 [Android] ProGuardでAndroidアプリを難読化していく手順(良い方法があったら知りたい)
  7. 2016/04/30 [Android] AndroidのAccountManagerをとりあえず動かすところまで
  8. 2011/09/30 [Java] [Windows] [Ruby] .msgファイルをパースして中から添付ファイルを抜き出す
  9. 2011/01/12 [Java] [Wicket] [イベント] Wicket勉強会に参加しました
  10. 2010/08/11 [Java] [イベント] JVM勉強会に行ってきました
  11. 2009/03/26 [Java] [Eclipse] Eclipseで優先順位の低いパッケージを補完候補から除外してみた
  12. 2009/03/16 [Java] [Wicket] Wicketでn行m列で折り返すリストを作る
  13. 2009/03/12 [Java] [Wicket] CheckBoxMultipleChoiceとChoiceRendererを使用したチェックボックスの比較
  14. 2009/03/11 [Java] [Wicket] 自作のWicketサンプルを上げてみる
  15. 2009/03/11 [Java] [Wicket] チェックボックスの初期値の続き