趣向を変えて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.testandroid.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

  • Eclipseやcommand prompt上でtest結果を見ることが出来る

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で確認

こんな感じ(緑はOK。赤だとNGでFailure Traceでどこのassertが失敗したか確認できる)

その他

  • 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