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

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


## 環境
OS X 
Android Studio 1.0.2


## テスト用サンプルアプリ
入力された値を足して表示するだけのサンプルアプリ（アクティビティを跨いだテストもしたいので2画面構成）。
![2015-01-24 23.50.31.png](https://qiita-image-store.s3.amazonaws.com/0/47437/f9c97ea6-bbd4-1b9c-4bc5-723b890b768e.png)　

![2015-01-24 23.50.50.png](https://qiita-image-store.s3.amazonaws.com/0/47437/50c1048e-a115-f6ca-4de9-8f30f67415fb.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に追記

```groovy: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

**サンプルテストコード**

```java: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


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


```zsh
./gradlew connectedAndroidTest 
``` 

**テスト結果レポート**


```zsh
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アプリのテストについて](http://qiita.com/shikato/items/e4fa620f0f616d6b790d)

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

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

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


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

1. Projectのbuild.gradleに以下を追記

```groovy:build.gradle 
classpath 'org.robolectric:robolectric-gradle-plugin:0.14.+' 
```
https://github.com/shikato/AndroidTestSample/blob/master/build.gradle

2. Appのbuild.gradleに以下を追記 

```groovy: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


**サンプルコード**  

```java: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


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

```zsh
./gradlew clean test
``` 

**テスト結果レポート**

```zsh 
app/build/test-report/index.html
``` 

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


## UIのテスト 

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

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

* Android Instrumentation
* Espresso
* Robotium 
* etc..

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

* UIAutomator
* Appium 
* etc..


### ActivityInstrumentationTestCase2（Android Instrumentation）
最初から使える、アクティビティをテストするためのクラス。 

**導入**
最初から使える


**テストコード** 

```java: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

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

```zsh
./gradlew connectedAndroidTest 
``` 


**テスト結果レポート**

```zsh 
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に追記

```groovy: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

**テストコード**

```java: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

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

```zsh
./gradlew connectedAndroidTest 
``` 


**テスト結果レポート**

```zsh
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アプリのテストについて](http://qiita.com/shikato/items/e4fa620f0f616d6b790d)

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

**導入**

```groovy: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


**テストコード**

```java: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

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

```zsh
./gradlew connectedAndroidTest 
``` 

**テスト結果レポート**

```zsh
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とかに記述していると、

```zsh
export _JAVA_OPTIONS="-Dfile.encoding=UTF-8"
```
Javaコマンドを実行する際、一行目に毎度

```zsh
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
```
という出力がされるけど、これのせいではまった。

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

```zsh
Error: Could not get the Java version. Is Java installed?
```
appium-doctorツールでは「All Checks were successful」と表示されているのに何故だろうとソースコードを確認してみると、

```javascript:/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行目でチェックしており、自分の環境だと上述したように一行目には

```zsh
Picked up _JAVA_OPTIONS: -Dfile.encoding=UTF-8
```
が出力されていたため、チェックに失敗していたことがわかった。
なので、以下をコメントアウトしたらInspectorが起動できた。 

```zsh 
if (javaVersion === null) {                                                                                                                                                                                                
  return cb(new Error("Could not get the Java version. Is Java installed?"));
}    
``` 
本来であれば
 
```zsh
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を指定しました）。

```javascript: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で操作した通りにアプリが自動的に動く。

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


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


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




## 結局どれを使うのか

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

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

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

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