趣向を変えてAndroid Testing
さて、今回はちょっと趣向を変えてAndroid Testingに関してのPostになります。
まず、Android Testingの概略をみて、Activity TestingのTutorialに沿ってTest codeを実装してみたいと思います
Android Testの基本(ここの概要)
Test Structure/Projects
- AndroidのTest FrameworkはJUnitを拡張
- TestはAppと同じようにProjectとして作成される
- Android toolを使用すればTest projectを作成してくれる
Test API
- JUnitを拡張
- Activity用、Provider用、Service用とcomponent specificなTest APIがある
- Assert classに加え正規表現がassertで使用出来るMoreAssertsとViewの位置をassert出来るViewAssertsがある
- System levelのclassをmockしたmock class群が存在する(android.testとandroid.test.mock内)
- MockApplication, MockContext, MockContentProvider, MockCursor, MockDialogInterface, MockPackageManager, MockResources, MockContentProvider, MockCursor等
- 上記classのcopy methodを使用するとUnsupportedOperationExceptionがthrowされるので、利用する際はoverrideする
Running Test
- Android test runner frameworkはJUnitのものを拡張
- Eclipse、もしくはcommand lineを使用しADBから実行することが出来る
- InstrumentationTestRunner classが重要なclass
- InstrumentationTestCaseをまとめて実行
こんな感じで実装
public class MyTestRunner extends InstrumentationTestRunner { @Override public TestSuite getAllTests() { final TestSuite testSuite = new InstrumentationTestSuite(this); testSuite.addTestSuite(SpinnerActivityTest.class); //他に実行するInstrumentationTestCase classがあればTestSuite#addTestSuiteで追加 return testSuite; } @Override public ClassLoader getLoader() { return MyTestRunner.class.getClassLoader(); } }
Seeing Test Results
What to Test
- Orientation change, Configuration change時の状態保持
- Battery consumption, Memory consumption, Storage consumption
Android JUnit Testの要点をまとめます
(今回はSpinnerActivityというSDKのSampleの中に入っているActivityをTest対象としてます)
1. EclipseでTest用Projectを作成する(File > New > Project > Android > Android Test Project)
-
- Project名はtest対象のprojectの最後に"Test"を加えたもの
- Package名はtest対象のpackageの最後に".test"を加えたもの
こんな感じです。基本的には"Test Project Name"を埋めてと"Test Target"を既存のTest対象Projectから選べば他はいじる必要ありません
-
- Test Projectが他のProjectと同様にPackage Explore上で見えるようになります
2. Test case classを作成する
-
- Class名はtest対象のclass名にTestを加えた物にする
- Test対象のActivity名をparameter化する
public class SpinnerActivityTest extends //Test対象のActivity名をparameter化する ActivityInstrumentationTestCase2<SpinnerActivity> {
3. setUp()methodをoverrideし、getActivity()でactivityを取得し、処理に必要なObjectを保持する
//setUp()は各test methodが呼ばれる「前」に、毎回呼ばれる @Override protected void setUp() throws Exception { super.setUp(); mActivity = (SpinnerActivity)getActivity(); mSpinner = (Spinner)mActivity.findViewById(com.android.example.spinner.R.id.Spinner01); mPlanetData = mSpinner.getAdapter(); }
4. test methodを実装し、内部でassert....()method群を使用し、test codeを記載
public void testPreConditions(){ assertTrue(mSpinner.getOnItemSelectedListener() != null); assertTrue(mPlanetData != null); assertEquals(mPlanetData.getCount(), ADAPTER_COUNT); }
5. Testを実行(TestProjectの上で右Click > Run As > Android JUnit Test)
6. Test結果をJUnit Paneで確認
その他
- initialzeが正しく行われているか確認するmethodは慣例的にtestPreConditions()と名づけるらしい
- Activity Testのおおまかな種類
- 初期設定test
- UI test
- State Management test
それでは実装です
AndroidManifest.xml(Test Projectを作成すると自動で生成されます。いじってません)
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.example.spinner.test" android:versionCode="1" android:versionName="1.0"> <uses-sdk android:minSdkVersion="3" /> <instrumentation android:targetPackage="com.android.example.spinner" android:name="android.test.InstrumentationTestRunner" /> <application android:icon="@drawable/icon" android:label="@string/app_name"> <uses-library android:name="android.test.runner" /> </application> </manifest>
ActivityInstrumentationTestCase2を拡張したclass
package com.android.example.spinner.test; import android.app.Instrumentation; import android.test.ActivityInstrumentationTestCase2; import android.test.UiThreadTest; import android.view.KeyEvent; import android.widget.Spinner; import android.widget.SpinnerAdapter; import android.widget.TextView; import com.android.example.spinner.SpinnerActivity; public class SpinnerActivityTest extends //Test対象のActivity名をparameter化する ActivityInstrumentationTestCase2<SpinnerActivity> { public static final int ADAPTER_COUNT = 9; public static final int INITIAL_POSITION = 0; public static final int TEST_POSITION = 5; public static final int TEST_STATE_DESTROY_POSITION = 2; public static final String TEST_STATE_DESTROY_SELECTION = "Earth"; public static final int TEST_STATE_PAUSE_POSITION = 4; public static final String TEST_STATE_PAUSE_SELECTION = "Jupiter"; private SpinnerActivity mActivity; private Spinner mSpinner; private SpinnerAdapter mPlanetData; private int mPos; private String mSelection; public SpinnerActivityTest() { super("com.android.example.spinner", SpinnerActivity.class); } //setUp()methodをoverrideし、getActivity()でactivityを取得し、処理に必要なObjectを保持する //setUp()は各test methodが呼ばれる「前」に、毎回呼ばれる @Override protected void setUp() throws Exception { super.setUp(); mActivity = (SpinnerActivity)getActivity(); mSpinner = (Spinner)mActivity.findViewById(com.android.example.spinner.R.id.Spinner01); mPlanetData = mSpinner.getAdapter(); } //tearDown()は各test methodが呼ばれる「後」に、毎回呼ばれる @Override protected void tearDown() throws Exception { super.tearDown(); //Nothing to do } //初期設定test //initialzeが正しく行われているか確認するmethodは慣例的にtestPreConditions()と名づけるらしい public void testPreConditions(){ assertTrue(mSpinner.getOnItemSelectedListener() != null); assertTrue(mPlanetData != null); assertEquals(mPlanetData.getCount(), ADAPTER_COUNT); } //UI test public void testSpinnerUI(){ //UI thread上で実行する必要がある mActivity.runOnUiThread(new Runnable(){ @Override public void run() { mSpinner.requestFocus(); mSpinner.setSelection(INITIAL_POSITION); } }); //Key eventを発行 sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); for(int i = 0; i < TEST_POSITION; i++){ sendKeys(KeyEvent.KEYCODE_DPAD_DOWN); } sendKeys(KeyEvent.KEYCODE_DPAD_CENTER); mPos = mSpinner.getSelectedItemPosition(); mSelection = (String)mSpinner.getItemAtPosition(mPos); TextView textView = (TextView)mActivity.findViewById(com.android.example.spinner.R.id.SpinnerResult); String resultText = (String)textView.getText(); //SpinnerでselectされたものとTextViewで表示されているものが一致するか確認 assertEquals(mSelection, resultText); } //State Management test //Applicationが終了し再起動した際に以前の状態を保持しているか確認 public void testStateDestroy(){ mActivity.setSpinnerPosition(TEST_STATE_DESTROY_POSITION); mActivity.setSpinnerSelection(TEST_STATE_DESTROY_SELECTION); mActivity.finish(); //ActivityInstrumentationTestCase2#getActivity()を実行するとApplicationが起動する mActivity = (SpinnerActivity)getActivity(); int currentPosition = mActivity.getSpinnerPosition(); String currentSelection = mActivity.getSpinnerSelection(); assertEquals(TEST_STATE_DESTROY_POSITION, currentPosition); assertEquals(TEST_STATE_DESTROY_SELECTION, currentSelection); } //State Management test //Applicationがforegroundからbackgroundに移動した際に以前の状態を保持しているか確認 //@UiThreadTestはUI thread上でtest codeを実行するというアノテーション @UiThreadTest public void testStatePause(){ //Instrumentaion objectを取得(Instrumentation objectからContextを取得するようなことも可能) Instrumentation mInstr = getInstrumentation(); mActivity.setSpinnerPosition(TEST_STATE_PAUSE_POSITION); mActivity.setSpinnerSelection(TEST_STATE_PAUSE_SELECTION); //直接Activity#onPause()を呼べる mInstr.callActivityOnPause(mActivity); mActivity.setSpinnerPosition(0); mActivity.setSpinnerSelection(""); //直接Activity#onResume()を呼べる mInstr.callActivityOnResume(mActivity); int currentPosition = mActivity.getSpinnerPosition(); String currentSelection = mActivity.getSpinnerSelection(); assertEquals(TEST_STATE_PAUSE_POSITION, currentPosition); assertEquals(TEST_STATE_PAUSE_SELECTION, currentSelection); } }
というような感じです。今回はTutorialに沿って試してみましたが、是非、実践でも使ってみたいなと思います
HashTag #Java, #Android, #JUnit, #ActivityInstrumentationTestCase2, #InstrumentationTestRunner, #Assert, #Spinner