SD Logger
``Android ユニットテスト実装についてのまとめ``
==============================================
ここでは、Android のアプリケーションに対し、ユニットテストを作成する方法について説明する。
概要
----
実行の流れ
~~~~~~~~~~
Android のユニットテスト開発手順を示す。
- TestCase を書く
- XXXTestCase を継承して書く。これがメインの仕事
- TestSuite を書く
- 普通はAllTests.java をコピーするだけ
- ビルドする
- make の場合は、TARGET_BUILD_VARIANT=tests を指定
- InstrumentationTestRunner を用いて、テストを実行する
- 起動は Dev Tool から行う
- logcat から結果を見る
実機で実行できるのは Instrumentation を継承したクラスである。 TestCase を実行するには、まず全ての TestCase を集めた
TestSuite (通常 AllTests.java) を作成し、これを InstrumentationTestRunner が実行する。
テストケースとは
~~~~~~~~~~~~~~~~
テストケースは、大きく分けて 2 種類ある。
1. Activity に対する TestCase: Junit を拡張した Instrumentation を用いる。JUnit
に加えて、テスト用のシステムの状態にアクセスできる。キー入力やタッチのような UI イベントを伝えられる。
- InstrumentationTestCase がベースクラス。これを継承した ActivityTestCase ほか
2. Activity 以外に対する TestCase : 普通の Junit の作法に従って書く。
- TestCase がベースクラス。これを拡張した、ApplicationTestCase, ServiceTestCase ほか
`1.` と `2.` とは使えるメソッドは異なるが、実行時にはどちらも `TestRunnerInstrumentation`_
が実行してくれる。 `1.` と `2.` に大きな違いはないため、同じパッケージに置いてもよい。
テスト環境内では、偽の context や intent を与えることができる。 これにより、実際の実機の状態とは異なる状態をシミュレートできる。
例えばバッテリなら、実際には満充電でも、バッテリ切れの状態をテストできる。
テストプログラムは、テスト対象のプログラムと同一プロセスで動く。これにより、テストプログラムとテスト対象のプログラムと情報をやり取りできる。Java
の言語を超えるようなこと、例えば private なメンバにアクセスしたりはできない。
Instrumentation とは
~~~~~~~~~~~~~~~~~~~~
Insterumentation とは、他のパッケージのクラス/Activity に対し操作をすることができる、特殊な activity 。
関係としては、C のプログラムとgdb などのデバッガに似ている。
普通は動作しているほかの Activity に対し、そのインスタンスを取得して、メソッドを呼んだりはできない。 (たとえば、他のプロセスで実行中の
String オブジェクトのインスタンスを、ほかのプロセスが取得することはできない) それが、Instumentation では、あらかじめ指定した
Activity のインスタンスを取得して、メソッドを呼び出すことができる。
Activity に対するテストでは、ライブラリのテストとは異なり、実機で実際に Activity を作成する必要がある。
このため、Instrumentation の仕組みを用いて、実機の実環境で Activity を作成し、そのインスタンスを取得した後、
クリックやキーイベントを注入することでテストを行う。
Instrumentation の機能単体でもテストを行うことができる。ただ、Instumentation 単体は起動してから終了するまでの一連の動作なの
で、ユニットテストのように「複数のテストを順番に実行する」仕組みではない。このため、「複数のテストを順番に実行してくれる」Instrumentation
が用意されている。これが `TestRunnerInstrumentation`_ である。
テストプロジェクトの作成
------------------------
作成内容
~~~~~~~~
Android のテストは、アプリ (apk) として作成する。 つまり、専用の Android.mk と、AndroiManifest.xml を持つ。
なおアプリの場合は、アプリの AndroidManifest.xml に混ぜて書くことも出来る。 このような例は、APIDemo に見ることができる。
ただし、製品にはテストコードを載せないことを考えると、別パッケージで 書くのが自然である。
Android.mk
~~~~~~~~~~
ポイントは以下の 4 つ。
- LOCAL_MODULE_TAGS は tests (開発中は eng の方が便利)
- LOCAL_JAVA_LIBRARIES として android.test.runner を指定する
- LOCAL_INSTRUMENTATION_FOR として、ターゲットのパッケージ名を書く
- LOCAL_PACKAGE_NAME は独自に設定 (Test っぽい名前をつけるのがよい)
::
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE_TAGS := tests
LOCAL_JAVA_LIBRARIES := android.test.runner
LOCAL_PACKAGE_NAME := NfxUpdaterTest
LOCAL_INSTRUMENTATION_FOR := NfxUpdater
LOCAL_SRC_FILES := $(call all-java-files-under) $(call all-Iaidl-
files-under)
include $(BUILD_PACKAGE)
include $(call all-makefiles-under, $(LOCAL_PATH))
※ 入れ子になったパッケージで、かつ ServiceTestCase などのクラスを用いる場合、 親のパッケージの Android.mk
で以下の指定が必要だった。
::
LOCAL_JAVA_LIBRARIES := android.test.runner
AndroidManifest.xml
~~~~~~~~~~~~~~~~~~~
タグで、テストを走らせる設定をする。 instrumentaion タグで指定するのは以下の 3 つである。
- Instrumentation (普通は InstrumentationTestRunner )
- Instrumentation で走らせるパッケージ
- その Insteumentation のラベル
ユニットテストの場合は、走らせるのは通常 android.test.InstrumentationTestRunner である。 もし独自に
Instrumentation を実装した場合は、それを記述する。 ここに登録した Instrumentation は、Dev Tool の
Instrumentation 一覧に表示される。
パッケージの指定は、ターゲットのパッケージ名を 指定する。(この場合は、com.sony.nfx.app.updater)
また、uses-library で test runner を指定する。
::
置き場所
~~~~~~~~
基本はどこにおいてもよい。 アプリの場合は、そのアプリの中に tests というディレクトリを作り、 その中に作るのが一般的なようである。 (参考:
packages/apps/Browser など)
::
- NfxUpdater + Android.mk
|
+ AndroidManifest.xml (アプリの設定)
|
+ src + ... (アプリのソース)
|
` tests + Android.mk
|
+ AndroidManifest.xml
(テストの設定)
|
+ src + ... (テストのソース)
ユニットテストプロジェクトの実装
--------------------------------
何をテストするか
~~~~~~~~~~~~~~~~
サービスやライブラリの場合は、メソッドごとのテストを行う。 Activity の場合は、ボタン操作、キーイベントに対する反応をテストする。
TestRunnerInstrumentation
~~~~~~~~~~~~~~~~~~~~~~~~~
Android のテストで実行するのは Instrumentation である。 ユニットテストの場合は、TestCase を
InstrumentationTestRunner に 与えることで実行される。
Instrumentation は、activity や service と同様に、AndroidManifest.xml への記述が必要である。(先述)
TestSuite
~~~~~~~~~
通常はそのパッケージ以下、全ての TestCase を含む Test Suite を作成する。 以下の二つの書き方がある。
::
public class AllTests implements TestSuiteProvider {
public TestSuite get!TestSuite() {
return new !TestSuiteBuilder(getClass())
.includeAllPackagesUnderHere()
.build();
}
}
::
public class AllTests extends TestSuite {
public static Test suite() {
return new !TestSuiteBuilder(getClass())
.includeAllPackagesUnderHere()
.build();
}
}
これらのコードは、そのパッケージ (ディレクトリ) 以下の TestCase のインスタンスを全て再帰的に調べ、 TestSuite に追加している。
TestCase の実装
~~~~~~~~~~~~~~~
ActivityUnitTestCase / ApplicationTestCase / ServiceTestCase / TestCase
などを継承して実装する。 例えば Activity のテストなら、ActivityUnitTestCase < テストしたいクラス名 >
として書く。実行中の activity は、getActivity() で取得できるので、外部から finish() で終了させたり、context
を取得していじったりできる。下に例を示す。
::
class MyActivityUnitTest extends
ActivityUnitTestCase {
protected void setUp(){
...
}
protected void testHogehoge(){
...
assertTrue(...);
}
protected void tearDown(){
...
}
}
書き方は Junit と同じである。
- まず事前の設定を setUp() で行う
- testXXX() というメソッドを実装する。これは、何か対象クラスのメソッドを呼び、assertXXX
で変化を確認する、というスタイルが基本である。
- tearDown() で終了処理をする
テストケースで実行されるメソッドは、以下の条件を満たす必要がある。
- public である
- 名前が test で始まる
- 引数なし
- 返り値なし
なお、アノテーションを使う場合は、こうした関数名の制約を受けない。
サービスのテスト (ServiceTestCase)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
サービスのテストは、`ServiceTestCase` を継承して作る。 Android のサービスが持つ機能を以下に挙げる。
- context から作成、破棄を支持できる
- Binder を介したリモート呼び出しが出来る
- Context を用いて、システムの機能 (ファイルシステムなど) にアクセスできる
通常のサービスは context#startService() によって、 Binder の向こう側に作られる。 サービスのコンストラクタ (new
Service() ) をユーザが明示的に呼び出すことはない。 Service の関数は、aidl から作られる Service.iface にあるものを
binder 越しに呼び出す。
これに対し、`ServiceTestCase` では、service はコンストラクタから明示的に作られる。 onCreate() や
onStart() も自動では呼ばれず、テスト中に明示的に呼び出す。 `ServiceTestCase` からは、Service
内部の関数も自由に呼び出すことができる。
以下に `ServiceTestCase` の例を示す。
::
class MyServiceTest extends ServiceTestCase {
/* コンストラクタは引数なしで作る */
public MyServiceTest() {
super(MyService.class); /* 対象のサービスの */
}
public void setUp(){
/* 全テストに共通な初期化を書く */
setContext( 作成した偽context );
}
public void tearDown(){
/* 全テストに共通な終了処理を書く */
}
public void testA(){
startService();
IBinder service = bindService();
IMyService iface =
IMyService.Stub.asInterface(service);
iface.myMethod(); /* 適当にメソッドを呼ぶ*/
assertXXX(); /* 適当に assert する*/
shutdownService();
}
public void testB(){
....
/* その他のテストも同様に実装 */
}
}
なお、重要なポイントとして、テスト対象のサービスは getService() を以下のように実装する必要がある。
::
public MyService {
Binder mBinder = new Binder {
MyService getService() {
return MyService.this;
}
}
public IBinder onBind(){
return mBinder;
}
}
偽 Context, 偽 Intent
~~~~~~~~~~~~~~~~~~~~~
Activity は、Intent を受け取って状態変化 (起動など) し、Context から情報を取得 (getService() など) し、DB
やファイルを操作する。テストでは、実機のグローバルな Context や Intent を触るのではなく、テスト用の Conext
に対して実験を行える。一方で、他のアプリと協調させたい場合は、実機の Context を与えてテストを行う。その他、テスト中では、偽のDB
やファイルを与えることができる。
テストの実行
------------
テストは実機 (またはエミュレータ) で実行し、出力は Logcat 経由でホスト (PC) から確認する。
実行は、ActivityManager に対し、Instrumentation と、テストパッケージを指定して行う。 先述したように、実行されるのは
Instrumentation なので、TestCase は TestSuite (AllTests.java)
で束ねて、InstumentationTestRunner で実行する。
実際の操作としては、実機画面で Dev Tool から実行する方法と、adb から直接 ActivityManager にコマンド発行する方法がある。
実機画面で実行
~~~~~~~~~~~~~~
実機本体からは、メニューから Dev Tools を起動し、Instrumentation メニューを選択し、並んでいる Instrumentation
を実行できる。 AndroidManifest.xml に タグがあると、自動的にこのメニューに追加される。
コマンドラインでの実行
~~~~~~~~~~~~~~~~~~~~~~
ホスト pc からの adb 操作でも実行できる。 引数では、 タグで指定したのと同じ情報を与える。
::
$ adb shell am instrument -w テスト対象/テスト
"APIDemo" 実行
::
$ adb shell am instrument -w\
com.example.android.apis/\
com.example.android.apis.app.LocalSampleInstrumentation
"BrowserTest" 実行
::
$ adb shell am instrument -w\
com.android.browser/\
com.android.browser.BrowserFunctionalTestRunner
結果の取得
~~~~~~~~~~
いずれの方法で起動した場合も、結果は logcat で観察できる。 たとえば以下。
::
INFO/TestRunner(332): started: testAndroid!TestCaseSetupProperly(com.
sony.nfx.app.updater.tests.UpdaterUtilTest)
INFO/instrumentation(323): INSTRUMENTATION_STATUS_RESULT:
id=InstrumentationTestRunner
INFO/instrumentation(323): INSTRUMENTATION_STATUS_RESULT: current=2
INFO/instrumentation(323): INSTRUMENTATION_STATUS_RESULT:
class=com.sony.nfx.app.updater.tests.UpdaterUtilTest
INFO/instrumentation(323): INSTRUMENTATION_STATUS_RESULT: stream=
INFO/TestRunner(332): finished: testAndroid!TestCaseSetupProperly(com
.sony.nfx.app.updater.tests.UpdaterUtilTest)
...
INFO/instrumentation(323): Test results for
InstrumentationTestRunner=..
INFO/instrumentation(323): Time: 0.012
INFO/instrumentation(323): OK (2 tests)
INFO/instrumentation(323): INSTRUMENTATION_CODE: -1
ソース
------
- まとめ `http://developer.android.com/intl/ja/guide/topics/testing/testing_android.html`
- adb からの instrumentation の走らせ方 `http://pdk.android.com/online-pdk/guide/instrumentation_testing.html`