朝練云々

諸事情により Android+CI で云々な件について確認着手。あと職場端末の chrome な cache が飛んだ。これはなかなかにキツいです。
メモなど追記の方向にて。

追記

ネタ的には

  • Android プロジェクトを Git で云々
  • Jenkins でテストをなんちゃら

あたり。つーことでローカルな Eclipse 君に EGit を導入して云々してみます。いきなり clone の方法が分かんなくて焦ったのですが、Package Explore 右クリックから import -> Git で良いのか成程。

ハロワから云々してみる

とりあえずハロワなプロジェクトを作成して EGit でリポジトリを作成。て、プロジェクト内に .git が作れんな。デフォの Use or create repository in parent folder of project にチェック入れると .git が作成される場所がさっぱり分かんなかったり。
とりあえず $HOME 配下に git というディレクトリを掘って云々して以下な形になっちゃいました。

  • Git Directory ~/git/HelloAndroid/.git
  • Working Directory ~/git/HelloAndroid

これってどうなんだろ。つうかワークスペース自体が移動になるのか。。一旦プロジェクトを eclipse 上から削除してみるか。全部削除。で、import してみたんですが駄目だな。あ、これって一旦 commit 作らんと駄目だったのか。

もっかい

手順は以下。

  • ハロワなプロジェクトを新規作成
  • プロジェクトを右 click して team -> share project を選択
  • New Repository を ~/git/HelloAndroid として作ります
  • src、libs、res および AndroidManifest.xml ならびに proguard-project.txt を index に追加
  • initial commit 作成

で、別場所に clone してみました。基本的な構成として

  • git が管理してるディレクトリ (ここでは HelloAndroid)
    • Android プロジェクト (ここでは HelloAndroid)
    • .git ディレクトリ

つうかこれって bare なリポジトリじゃないのかな。gitlab に push してみます。あ、今のまんまだとアレなので手を入れておくか。ダイアログに超適当なナニを入力したら push できちゃった模様。こんどはこれを取得すれば良いのか。あ、ローカルなソレも全部削除で良いのかな。
で、結局作業ディレクトリが ~/git 配下になっちゃうんですがいいのかなぁ。なんとなく見えかたも最初とは違いますね。むむむ。

テストプロジェクト作成

とりあえず細かい部分はスルーってことにして、ってやっぱデフォな場所にテスト対象なプロジェクトが無いと駄目だな。うーん。EGit が微妙なのか Android が微妙なのか。
ぐぬぬ、プロジェクト作成な部分から再度確認します。一回デフォでリポジトリ作って云々してみりゃ良いのかな。

  • ハロワなプロジェクト作成
  • デフォでリポジトリを作って index に追加して commit 作る
  • リモートリポジトリに push
  • いったん workspace 上からはプロジェクトを削除してリモートから clone

で動作確認を、って思ってたら eclipse 絶賛暴走中で困り果ててるのが今です。
そしてリモートから clone できたのですが、デフォな方法だと三階層くらい上から管理されてるカンジになっちゃってますね。何がしたいのかというと

  • ワークスペース上に直接プロジェクトを持ってきたい
  • そゆ意味ではプロジェクトの中に .git が居て欲しい

んですが駄目なのかなぁ。結論としてはリポジトリの作成についてはコマンド使います、ってことで良いのかどうか。そろそろ疲れてきたので次に進みたいです。

  • プロジェクト新規作成
  • ディレクトリの中で git init 実行
  • EGit 使わずに index に追加して commit 作って push までやる

で、それを import するのか。

  • 一旦 workspace 上にあるプロジェクトを削除
  • workspace 上に import

してみたんですが Android プロジェクトとして認識していないご様子。

正しくは

先に clone してるカンジだったので existing project としてナニすれば良かった模様。試験プロジェクト作成時にもこれで読み込みできました。
つーことは

  • 基本的にプロジェクト作成時にはコマンドでリモートに push まで実施
    • git init および git remote add
    • git add (index への追加)
    • git commit (最初の commit 作成)
    • git push
  • その後、EGit でプロジェクトを取得
    • import でリモートにあるリポジトリを取得
    • use the New Project wizard を使用
    • wizard で Android Project from Existing Code を選択して clone した場所を指定
      • clone した場所はリポジトリ指定時に指定しますので覚えておく必要あり

おそらくこれで問題なし。

ようやくテストプロジェクトが作れます。先に作ったプロジェクトをテストする形で新規作成したらこちらも一旦コマンドベースで云々。

  • git init して git remote add
  • AndroidManifest.xml proguard-project.txt assets/ res/ src を index に追加
  • commit して push

で、プロジェクトを削除して取得か。

その後

テスト作成開始したのですがテスト対象プロジェクトなクラスが見えぬ。プロジェクトのプロパティの Java Build Path の Projects に追加で解決。
そしてテストケースなナニのコンストラクタが deprecate てorz

さらに

setup および試験を盛り込んで JUnit なソレを動かそうとしてるんですが、エミュレータがアレorz
えええ、って言ってたのですがコンストラクタの引数を無い形に修正すると楽勝で動きました。何それorz
つうか Jenkins で自動で試験とかこれ無理じゃないですかね。おそらく違う試験エンジンを使うんだろうな。とりあえずテスト部の記事を見つつ確認続行。む、Robolectric てヤツはエミュレータ使わんのか。成程。
Activity の試験のために、ということで以下三点が用意されているとのこと。

  • ActivityInstrumentationTestCase2
    • モックオブジェクトが使えない模様
    • getActivity でオブジェクト取得可能
    • setActivityIntent でインテントを扱える
  • ActivityUnitTestCase
    • モックのContextやApplicationを差し込むことができる
    • startActivity(Intent, Bundle, Object) でアクティビティの取得が可能
    • onCreate のみが呼ばれ、onStart や onResume は呼ばれない
  • SingleLaunchActivityTestCase
    • getActivity で同じオブジェクトが取得できる

さっきのソレはいっちゃん上のヤツを使ったんですね。あるいは ContentProvider や Service の試験、なんてのも可能なのか。

ビジネスロジックの試験

StringUtils というクラスを追加する模様。

package com.example.helloandroid;

public class StringUtils {
    public static boolean isBlank(CharSequence cs) {
        int strLen;
        if (cs == null || (strLen = cs.length()) == 0) {
            return true;
        }
        for (int i = 0; i < strLen; i++) {
            if (!Character.isWhitespace(cs.charAt(i))) {
                return false;
            }
        }
        return true;
    }
}

で、試験も追加。単純に New -> Junit Test Case で StringUtilsTest を追加。

package com.example.helloandroid.test;

import junit.framework.TestCase;
import com.example.helloandroid.*;

public class StringUtilsTest extends TestCase {
    public void testIsBlank() throws Exception {
        assertTrue(StringUtils.isBlank(null));
        assertTrue(StringUtils.isBlank(""));
        assertTrue(StringUtils.isBlank(" "));
        assertFalse(StringUtils.isBlank("bob"));
        assertFalse(StringUtils.isBlank("  bob  "));
    }
}

試験 green 確認。次は AndroidTestCase とモックを使って、とあります。
てそういえば branch して云々、ってのを全然してないことに今更気づくなど。

UI テスツ。もっかい最初から云々ってことで。ええと要件は以下として

  • テスト対象プロジェクトは com.example.androiduitest
  • 一つ Activity を持ってて名前は MainActivity
  • テストプロジェクトのパケジは com.example.androiduitest.test
  • New -> package して New -> class した後に若干手を入れて以下な状態に
package com.example.androiduitest.test;

import android.test.ActivityInstrumentationTestCase2;
import com.example.androiduitest.*;

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {

    public MainActivityTest() {
        super(MainActivity.class);
        // TODO Auto-generated constructor stub
    }

}

で、まず EditText が空って試験を云々とのことで以下の状態になりました。

package com.example.androiduitest.test;

import android.app.Activity;
import android.test.ActivityInstrumentationTestCase2;
import android.widget.EditText;
import android.widget.TextView;

import com.example.androiduitest.*;

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
    private Activity activity;

    public MainActivityTest() {
        super(MainActivity.class);
    }

    public void testEditText_initialize() throws Exception {
        EditText edit = (EditText)activity.findViewById(com.example.androiduitest.R.id.editer);
        TextView result = (TextView)activity.findViewById(com.example.androiduitest.R.id.result);
        assertEquals("", edit.getText().toString());
        assertEquals("", result.getText());
    }

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

        activity = getActivity();
    }
}

試験は green 終了を確認してます。次は Instrumentation なるナニが出てきました。ちょっとどんどんヤッツケて中身確認してみます。

package com.example.androiduitest.test;

import android.app.Activity;
import android.app.Instrumentation;
import android.test.ActivityInstrumentationTestCase2;
import android.view.KeyEvent;
import android.widget.Button;
import android.widget.EditText;
import android.widget.TextView;

import com.example.androiduitest.*;

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {
    private Activity activity;
    private Instrumentation instrumentation;

    public MainActivityTest() {
        super(MainActivity.class);
    }

    public void testEditText_initialize() throws Exception {
        EditText edit = (EditText)activity.findViewById(com.example.androiduitest.R.id.editer);
        TextView result = (TextView)activity.findViewById(com.example.androiduitest.R.id.result);
        assertEquals("", edit.getText().toString());
        assertEquals("", result.getText());
    }

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

        activity = getActivity();
        instrumentation = getInstrumentation();
    }
    
    public void testEditText_checkInput() throws Exception {
        final EditText edit = (EditText)activity.findViewById(com.example.androiduitest.R.id.editer);
        final Button button = (Button)activity.findViewById(com.example.androiduitest.R.id.send);
        TextView result = (TextView)activity.findViewById(com.example.androiduitest.R.id.result);

        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                edit.requestFocus(); // EditTextにフォーカスを当てる
            }
        });
        instrumentation.waitForIdleSync();
        // sendKeysで文字列を入力する
        sendKeys(KeyEvent.KEYCODE_F);
        sendKeys(KeyEvent.KEYCODE_O);
        sendKeys(KeyEvent.KEYCODE_X);
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                button.performClick();
            }
        });
        instrumentation.waitForIdleSync();
        assertEquals("入力された値は正しい","fox", result.getText().toString());
    }
}

へー、Activity なインスタンス持ってるので runOnUiThread でイベント起こしちゃってるのか。sendKeys なんてメソドも面白い。
あるいは UI Thread を意識しない方法なんてのもあるとのこと。
次。ActivityInstrumentationTestCase2 だとライフサイクルな callback 単発の試験ができないとのこと。こうしたナニは ActivityTestUnitCase というクラスを使えば良いらしい。もう一つ試験なクラスが追加になるのか。
以下な callback を追加して

    @Override
    protected void onStart() {
        super.onStart();
        state = 1;
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        state = 2;
    }

あるいは属性と getter 追加。

    private int state;
    
    public int getState() { return state; }

で、試験は以下。

package com.example.androiduitest.test;

import android.content.Intent;
import android.test.ActivityUnitTestCase;
import com.example.androiduitest.*;

public class MainActivityTest2 extends ActivityUnitTestCase<MainActivity> {

    public MainActivityTest2() {
        super(MainActivity.class);
    }

    public void testButton1() throws Exception {
        Intent intent = new Intent();
        MainActivity target = startActivity(intent, null, null);
        assertEquals("MainActivity#getState should return 0", 0, target.getState());
        getInstrumentation().callActivityOnStart(target);
        assertEquals("MainActivity#getState should return 1", 1, target.getState());
        getInstrumentation().callActivityOnResume(target);
        assertEquals("MainActivity#getState should return 2", 2, target.getState());
    }

}

試験なクラスって追加が結構厄介ですね。最後が画面遷移なテスツらしい。これ、追加したら片方の試験が red になりますね。追加するのは以下です。

    public void testButton() throws Exception {
        Intent intent = new Intent();
        MainActivity activity = startActivity(intent, null, null);
        final Button button = (Button)activity.findViewById(com.example.androiduitest.R.id.send);
        activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                button.performClick();
            }
        });
        getInstrumentation().waitForIdleSync();

        Intent target = getStartedActivityIntent();
        String ret = target.getComponent().getClassName();
        assertEquals(SecondAndroidActivity.class.getName(), ret);
            
        int request_code = getStartedActivityRequest();
        assertEquals(100, request_code);
    }

なんとなく今日の云々で

を云々、というあたりは確認できました。あとは HTTP な mock とか CI あたりか。