Androidのテスティングフレームワークを選定してみる

  • 220
    いいね
  • 2
    コメント
この記事は最終更新日から1年以上が経過しています。

Androidで良い感じにテストするために、たくさんあるテスティングフレームワークを試して選定してみる。

環境

OS X
Android Studio 1.0.2

テスト用サンプルアプリ

入力された値を足して表示するだけのサンプルアプリ(アクティビティを跨いだテストもしたいので2画面構成)。
2015-01-24 23.50.31.png 

2015-01-24 23.50.50.png

リポジトリ
https://github.com/shikato/AndroidTestSample

今回はこのアプリに対してテストする。

ロジックのテスト

Android Testing Framework

標準でJUnitベースのAndroid Testing Frameworkが使える。
これまではJUnit3ベースだったけど、最近JUnit4がAndroid support libraryに含まれるようになり、JUnit4な記述でも簡単に書けるようになった。

JUnit4の導入
1. Android Support Repositoryのバージョンを上げる(Rev11以上)。
2. build.gradleに追記

build.gradle
defaultConfig {
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
packagingOptions {
    exclude 'LICENSE.txt'
} 
dependencies {
    androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
}

https://github.com/shikato/AndroidTestSample/blob/master/app/build.gradle

サンプルテストコード

JUnit4Test.java
@RunWith(AndroidJUnit4.class)
public class JUnit4Test {

    private Context mContext;

    @Before
    public void setUp() throws Exception {
        // Contextを取得
        mContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
    }

    @Test
    public void add() {
        assertThat(Calc.add(3, 5), is(8));
    }

    @Test
    public void context() {
        // Contextを使ったテスト
        assertThat(mContext.getString(R.string.app_name), is("AndroidTestSample"));
    }
}

https://github.com/shikato/AndroidTestSample/blob/master/app/src/androidTest/java/org/shikato/androidtestsample/android/JUnit4Test.java

テストの実行
※実機を接続、もしくはエミュレータを起動しておく

./gradlew connectedAndroidTest 

テスト結果レポート

app/build/outputs/reports/androidTests/connected/index.html

感想
ロジックのテストするだけなら、これで十分な気がする。

参考
http://vividcode.hatenablog.com/entry/android-app/library/espresso-2.0

※ 2015-07-30追記
この記事の内容は少し古いです。以下記事で情報をupdateしています。
2015年7月時点でのJUnit4やEspressoを使ったAndroidアプリのテストについて

Robolectric

JVM上で動作するテスティングフレームワーク
http://robolectric.org/

メリット
JVM上で動作し、テスト実行時にエミュレータや実機にアプリをインストールするフローが入らないため速い。
JUnit4が使える(Android Testing Frameworkでもサポートライブラリから導入できるようになったので、今はありがたみは少ない)

デメリット
あくまでもJVM上でのモック(Shadow Objects)を使ったテストなので信憑性は低くなる。

導入
テンプレートプロジェクトがあるので、それを見れば導入しやすい。
https://github.com/robolectric/deckard-gradle

  1. Projectのbuild.gradleに以下を追記
build.gradle
classpath 'org.robolectric:robolectric-gradle-plugin:0.14.+' 

https://github.com/shikato/AndroidTestSample/blob/master/build.gradle

  1. Appのbuild.gradleに以下を追記
build.gradle
apply plugin: 'robolectric'

testCompile('junit:junit:4.12') {
    exclude module: "hamcrest-core"
}
testCompile('org.robolectric:robolectric:2.4') {
    exclude module: "classworlds"
    exclude module: "maven-artifact"
    exclude module: "maven-artifact-manager"
    exclude module: "maven-error-diagnostics"
    exclude module: "maven-model"
    exclude module: "maven-plugin-registry"
    exclude module: "maven-profile"
    exclude module: "maven-project"
    exclude module: "maven-settings"
    exclude module: "nekohtml"
    exclude module: "plexus-container-default"
    exclude module: "plexus-interpolation"
    exclude module: "plexus-utils"
    exclude module: "wagon-file"
    exclude module: "wagon-http-lightweight"
    exclude module: "wagon-http-shared"
    exclude module: "wagon-provider-api"
}

/* 以下は各々の環境に合わせる */ 
robolectric {
    include '**/java/**/*.class'
    exclude '**/android/**/*.class'
} 

https://github.com/shikato/AndroidTestSample/blob/master/app/build.gradle

サンプルコード

RobolectricTest.java
@Config(emulateSdk = 18)
@RunWith(RobolectricTestRunner.class)
public class RobolectricTest {

    @Before
    public void setup() {
    }

    @After
    public void teardown() {
    }

    @Test
    public void sample() {
        assertThat(
            Robolectric.application.getApplicationContext().getString(R.string.app_name), is("AndroidTestSample"));
    }
}

https://github.com/shikato/AndroidTestSample/blob/master/app/src/androidTest/java/org/shikato/androidtestsample/java/RobolectricTest.java

テストの実行
※実機を接続したりエミュレータを起動する必要はない

./gradlew clean test

テスト結果レポート

app/build/test-report/index.html

感想
テストが早く終わるのは良いけど、信憑性が気になる。

UIのテスト

UIテスティングフレームワークの種類としては主に以下の2タイプがある。

テスト対象アプリの署名がテストコードと合致する必要があるもの
自身がビルドできるアプリしかテストできない。

  • Android Instrumentation
  • Espresso
  • Robotium
  • etc..

署名に関係なくテストできるもの
他者のアプリでもテストできる。 ただ、できることは署名合致タイプと比較すると多分少ない。

  • UIAutomator
  • Appium
  • etc..

ActivityInstrumentationTestCase2(Android Instrumentation)

最初から使える、アクティビティをテストするためのクラス。

導入
最初から使える

テストコード

ActivityInstrumentationTestCaseTest.java
@RunWith(AndroidJUnit4.class)
@LargeTest
public class ActivityInstrumentationTestCaseTest extends ActivityInstrumentationTestCase2<TopActivity> {

    private Activity mActivity;

    public ActivityInstrumentationTestCaseTest() {
        super(TopActivity.class);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();
        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
        mActivity = getActivity();
    }

    @Test
    public void testAddition() {
        // 数字入力(num1)
        EditText num1 = (EditText)mActivity.findViewById(R.id.num1);
        TouchUtils.clickView(this, num1);
        sendKeys(KeyEvent.KEYCODE_1);

        // 数字入力(num2)
        EditText num2 = (EditText)mActivity.findViewById(R.id.num2);
        TouchUtils.clickView(this, num2);
        sendKeys(KeyEvent.KEYCODE_1);
        sendKeys(KeyEvent.KEYCODE_0);

        // ResultActivityの起動を監視
        Instrumentation.ActivityMonitor monitor =
                new Instrumentation.ActivityMonitor(ResultActivity.class.getCanonicalName(), null, false);
        getInstrumentation().addMonitor(monitor);

        // =ボタンクリック
        Button addBtn = (Button)mActivity.findViewById(R.id.equal_button);
        TouchUtils.clickView(this, addBtn);

        // 起動待ち
        Activity resultActivity = getInstrumentation().waitForMonitorWithTimeout(monitor, 3000L);

        // ResultActivityが起動したか確認
        assertThat(monitor.getHits(), is(1));
        assertThat(resultActivity, notNullValue());

        // 計算結果確認
        TextView result = (TextView)resultActivity.findViewById(R.id.result);
        assertThat(result.getText().toString(), is("11"));
    }


    @After
    public void tearDown() throws Exception {
        super.tearDown();
    } 

https://github.com/shikato/AndroidTestSample/blob/master/app/src/androidTest/java/org/shikato/androidtestsample/android/ActivityInstrumentationTestCaseTest.java

テストの実行
※実機を接続、もしくはエミュレータを起動しておく

./gradlew connectedAndroidTest 

テスト結果レポート

app/build/outputs/reports/androidTests/connected/index.html

感想
導入は簡単だけど、記述が冗長な気がする。

Espresso

Googleが開発している「スクリーンの中の対象UIをみつけ」「何かアクションをし」「その結果を確認する」という、ユーザのアクションに沿ったUIテスティングフレームワーク。
Android Instrumentationベース。
https://code.google.com/p/android-test-kit/wiki/Espresso
http://wazanova.jp/post/62856507585/espresso-android-ui-gtac-2013

最近ver2.0がリリースされAndroid support libraryに含まれるようになり、導入が楽になった(JUnit4もこれの恩恵)。
https://code.google.com/p/android-test-kit/wiki/ReleaseNotes

導入
1. Android Support Repositoryのバージョンを上げる(Rev11以上)。
2. build.gradleに追記

build.gradle
defaultConfig {
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
packagingOptions {
    exclude 'LICENSE.txt'
} 
dependencies {
    compile 'com.android.support:support-annotations:21.0.3'
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.0'
    androidTestCompile 'com.android.support.test:testing-support-lib:0.1'
}

https://github.com/shikato/AndroidTestSample/blob/master/app/build.gradle

テストコード

EspressoTest.java
@RunWith(AndroidJUnit4.class)
public class EspressoTest extends ActivityInstrumentationTestCase2<TopActivity> {

    private Activity mActivity;

    public EspressoTest() {
        super(TopActivity.class);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();
        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
        mActivity = getActivity();
    }

    @Test
    public void testAddition() {
        // 数字入力
        onView(ViewMatchers.withId(R.id.num1)).perform(typeText("2"));
        onView(withId(R.id.num2)).perform(typeText("20"));

        // 足すボタンクリック
        onView(withId(R.id.equal_button)).perform(click());

        // 計算結果確認
        onView(withId(R.id.result)).check(matches(withText("22")));
    }

    @After
    public void tearDown() throws Exception {
        super.tearDown();
    }
} 

https://github.com/shikato/AndroidTestSample/blob/master/app/src/androidTest/java/org/shikato/androidtestsample/android/EspressoTest.java

テストの実行
※実機を接続、もしくはエミュレータを起動しておく

./gradlew connectedAndroidTest 

テスト結果レポート

app/build/outputs/reports/androidTests/connected/index.html

感想
ActivityInstrumentationTestCase2を使用した時よりもかなり簡潔に書けた。
support libraryにも含まれたし、今はEspressoの流れ?

参考
http://vividcode.hatenablog.com/entry/android-app/library/espresso-2.0

※ 2015-07-30追記
この記事の内容は少し古いです。以下記事で情報をupdateしています。
2015年7月時点でのJUnit4やEspressoを使ったAndroidアプリのテストについて

Robotium

レイアウトの構造を知らなくてもUIを操作できるUIテスティングフレームワーク。
Android Instrumentationベース。
https://code.google.com/p/robotium/

導入

build.gradle
defaultConfig {
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

dependencies {
    androidTestCompile 'com.jayway.android.robotium:robotium-solo:5.2.1'
}

https://github.com/shikato/AndroidTestSample/blob/master/app/build.gradle

テストコード

EspressoTest.java
@RunWith(AndroidJUnit4.class)
@LargeTest
public class RobotiumTest extends ActivityInstrumentationTestCase2<TopActivity> {

    private Activity mActivity;

    public RobotiumTest() {
        super(TopActivity.class);
    }

    @Before
    public void setUp() throws Exception {
        super.setUp();
        injectInstrumentation(InstrumentationRegistry.getInstrumentation());
        mActivity = getActivity();
    }

    @Test
    public void testAddition() {
        Solo solo = new Solo(getInstrumentation(), mActivity);

        // 1つめのEditTextに1と入力
        solo.enterText(0, "3");
        // ID指定も可能
        // solo.enterText((EditText)mActivity.findViewById(R.id.num1), "3");

        // 2つめのEditTextに30と入力
        solo.enterText(1, "30");

        // 足すと書かれたボタンを押す
        solo.clickOnButton("=");

        // ResultActivity の起動を確認
        solo.assertCurrentActivity("ResultActivity now.", ResultActivity.class);

        // 計算結果である33と書かれたテキストを確認
        assertThat(solo.searchText("33"), is(true));
    }

    @After
    public void tearDown() throws Exception {
        super.tearDown();
    }
}

https://github.com/shikato/AndroidTestSample/blob/master/app/src/androidTest/java/org/shikato/androidtestsample/android/RobotiumTest.java

テストの実行
※実機を接続、もしくはエミュレータを起動しておく

./gradlew connectedAndroidTest 

テスト結果レポート

app/build/outputs/reports/androidTests/connected/index.html

感想
以下記事では、コアの開発が止まっているなど、あまり良いことが書かれていないのが気になる。
http://wazanova.jp/items/1623

UIAutomator

Android SDK付属の、Android 4.1 以降で動作するUIテスティングフレームワーク。
http://developer.android.com/tools/help/uiautomator/index.html

他者が署名したアプリでもテストできるので、広く配布しているライブラリのテストとかに良さそう。
試そうかと思ったけど、Appiumのが面白そうなので、そっちを試す。

導入するならmixiのAndroidTrainingが参考になりそう(Eclipseだけど)
http://mixi-inc.github.io/AndroidTraining/fundamentals/2.11.testing.html#UIAutomator

Appium

Android、iOS、FireFoxOSに対応しているUIテスティングフレームワーク。
テストコードはJavaだけでなく、Node.jsやRubyなどいろんな言語で書ける。
http://appium.io/

使える言語

  • Node.js
  • Ruby
  • Python
  • Java
  • JavaWcript
  • PHP
  • C#

AppiumはAndroidに関してはUIAutomatorを内部で使用している。
http://www.publickey1.jp/blog/14/javascriptselenium1_selenium_1.html

導入
以下を参考に、もろもろ構築しました。
http://qiita.com/kzm7/items/4386f37434c39b6b8095
http://qiita.com/kzm7/items/d3bdebd3930860d0b473
http://appium.io/slate/en/master/?ruby#running-appium-on-windows

はまったとこ
Javaの文字コードをセットするため以下を.zshrcとかに記述していると、

export _JAVA_OPTIONS="-Dfile.encoding=UTF-8"

Javaコマンドを実行する際、一行目に毎度

Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8

という出力がされるけど、これのせいではまった。

Appium.appの虫眼鏡ボタンをクリックすると、Inspectorが起動するはずなんだけど、そこで以下の様なエラーがでて起動できなかった。

Error: Could not get the Java version. Is Java installed?

appium-doctorツールでは「All Checks were successful」と表示されているのに何故だろうとソースコードを確認してみると、

/Applications/Appium.app/Contents/Resources/node_modules/appium/lib/devices/android/android-common.js
androidCommon.getJavaVersion = function (cb) {
  exec("java -version", function (err, stdout, stderr) {
    var javaVersion = null;
    if (err) {
      return cb(new Error("'java -version' failed. " + err));
    } else if (stderr) {
      var firstLine  = stderr.split("\n")[0];
      if (new RegExp("java version").test(firstLine)) {
        javaVersion = firstLine.split(" ")[2].replace(/"/g, ''); 
      }    
    }    
    if (javaVersion === null) {                                                                                                                                                                                                
      return cb(new Error("Could not get the Java version. Is Java installed?"));
    }    
    return cb(null, javaVersion);
  });  
};

となっていて、java -versionコマンド実行結果の1行目でチェックしており、自分の環境だと上述したように一行目には

Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8

が出力されていたため、チェックに失敗していたことがわかった。
なので、以下をコメントアウトしたらInspectorが起動できた。

if (javaVersion === null) {                                                                                                                                                                                                
  return cb(new Error("Could not get the Java version. Is Java installed?"));
}    

本来であれば

Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8

が表示されないようにしたいところだけど、文字コードを設定したまま消す、というのは面倒そうな感じなのでやめました。。。
http://stackoverflow.com/questions/11683715/suppressing-the-picked-up-java-options-message

Appium.app(GUI)の実行
以下が参考になります。
http://qiita.com/kzm7/items/d3bdebd3930860d0b473

Appium.appのInspectorでテスト対象アプリを操作すると以下のようなコードが出力される(言語はNode.jsを指定しました)。

appium_sample.js
"use strict";

var wd = require("wd");
var chai = require("chai");
var chaiAsPromised = require("chai-as-promised");

chai.use(chaiAsPromised);
chai.should();
chaiAsPromised.transferPromiseness = wd.transferPromiseness;

var desired = {
  "appium-version": "1.0",
  platformName: "Android",
  platformVersion: "4.4",
  deviceName: "mydevice",
  app: "{{apkのpath}}",
  "app-package": "org.shikato.androidtestsample",
  "app-activity": ".TopActivity"
};

var browser = wd.promiseChainRemote("0.0.0.0", 4723);
browser.init(desired).then(function() {
  return browser
    .elementByXPath("//android.view.View[1]/android.widget.FrameLayout[2]/android.widget.LinearLayout[1]/android.widget.EditText[1]").sendKeys("12")
    .elementByXPath("//android.view.View[1]/android.widget.FrameLayout[2]/android.widget.LinearLayout[1]/android.widget.EditText[2]").sendKeys("5")
    .elementByXPath("//android.view.View[1]/android.widget.FrameLayout[2]/android.widget.LinearLayout[1]/android.widget.Button[1]").click()
    .fin(function() {
      return browser.quit();
    });
}).done(); 

https://gist.github.com/shikato/e05d1aa7fde30db15a07

これを実行してやると、Inspectorで操作した通りにアプリが自動的に動く。

node appium_sample.js

ただ、これだけではテストにならないので、Node.jsならNode.jsのテスティングフレームワークを使って、テストできる形式に修正していく。

長くなってきたので、Appiumに関しては続きを別の記事にでも書きます。。。

感想
Node.jsとかでテストを書けたり、他者のアプリでもテストできるのが良い。

結局どれを使うのか

とりあえず、以下を使ってみる予定です。

ロジックのテスト

Android Testing Framework (JUnit4)
JUnit4が簡単に使えるようになったし、Robolectricだと信憑性の問題から結局、他でもテストしないといけなそうなので。

UIのテスト

Espresso (自分でビルドできるアプリをテストする場合)
簡潔に記述できるし、Android support libraryにも含まれるようになり、今後もある程度安心して使えそうなので。

Appium(自分でビルドできないアプリをテストする場合)
最近ライブラリを開発する機会があり、そのライブラリを導入しているアプリのテストをするのが楽になりそうなので。