Androidアプリのテスト関係な件

2013-01-27T00:00:00+00:00 Android Java

色々まとめてみようかと思う。以前勉強したのをAndroid v4.1ベースで検証して書く

前提

アプリとテストはプロジェクトを分ける。テストプロジェクトはアプリプロジェクトを参照として設定する(普通にADT使えば特にやる必要ない。テストプロジェクト作成段階で設定される)

で以下もテストプロジェクト作成段階で設定されるので必要ないけど

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="sample.test.test"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="16" android:maxSdkVersion="16" android:targetSdkVersion="16" />

    <instrumentation android:name="android.test.InstrumentationTestRunner" android:targetPackage="sample.test" />

    <application android:icon="@drawable/ic_launcher" android:label="@string/app_name" android:allowBackup="false">
        <uses-library android:name="android.test.runner" />
    </application>

</manifest>

な感じで<instrumentation>と<uses-library>とかの設定が必要

あとはクラス書きつつテストケースを書く感じ。でテストを対象とするクラスはアプリプロジェクト、テストケースはテストプロジェクトに書く。でテストケースの方式はJUnit4方式じゃなくてJUnit3方式

あとテストプロジェクトのアプリもしっかり端末に入るので(ry

ApplicationTestCase

android.app.Applicationを拡張して使用する場合、ApplicationTestCaseを使えばAndroid JUnitでテスト出来る。例えば以下のようなApplicationクラスを継承したクラスがあった場合

package sample.test;

import android.app.Application;

public class MainApplication extends Application {
    public String getAppName() {
        return getResources().getString(R.string.app_name);
    }
}

getAppNameメソッドをテストしたい場合

package sample.test;

import android.app.Application;
import android.test.ApplicationTestCase;

public class MainApplicationTestCase extends ApplicationTestCase<MainApplication> {

    public MainApplicationTestCase() {
        super(MainApplication.class);
    }

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

        createApplication();
    }

    public void test_getAppNameのテスト() {
        Application app = getApplication();
        assertNotNull(app);
        assertTrue(app instanceof MainApplication);

        assertEquals("sample", ((MainApplication)app).getAppName());
    }
}

な感じでテスト出来る

AndroidTestCase

AndroidのContextに依存するようなクラスをテストするケースにおいてはAndroidTestCaseを使えば良い模様。

package sample.test;

import android.test.AndroidTestCase;

public class MainAndroidTestCase extends AndroidTestCase {
    public void test_getAppName() {
        String appName = getContext().getString(R.string.app_name);
        assertNotNull(appName);
        assertEquals("sample", appName);
    }
}

で、同様な例としてBroadcastReceiverをテストする場合等でもAndroidTestCaseを使えば出来なくもない。それは以前に書いたので省略

ServiceTestCase

android.app.Serviceをテストする目的で使用。とりあえずてきとーに

package sample.test;

import java.util.ArrayList;
import java.util.List;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

public class SampleService extends Service {

    private List<String> names;
    private boolean completed = false;

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flag, int startId) {
        super.onStartCommand(intent, flag, startId);

        names = new ArrayList<String>();

        new Thread() {
            @Override
            public void run() {
                names.add("hoge");
                names.add("fuga");
                names.add("foobar");

                completed = true;
            }
        }.start();

        return START_STICKY;
    }

    public List<String> getNames() {
        return names;
    }

    public boolean isCompleted() {
        return completed;
    }
}

な感じで作っといて、onStartCommand等で実行される処理をテストする際に

package sample.test;

import java.util.List;

import android.app.Service;
import android.content.Intent;
import android.test.ServiceTestCase;

import sample.test.SampleService;

public class SampleServiceTestCase extends ServiceTestCase<SampleService> {

    public SampleServiceTestCase() {
        super(SampleService.class);
    }

    public void testSampleService() {
        startService(new Intent(getContext(), SampleService.class));

        Service service = getService();
        assertNotNull(service);
        assertTrue(service instanceof SampleService);

        SampleService ss = (SampleService)service;

        try {
            do {
                Thread.sleep(1000);
            } while (!ss.isCompleted());
        } catch(InterruptedException e) {
            e.printStackTrace();
        }

        List<String> names = ss.getNames();
        assertNotNull(names);
        assertEquals(3, names.size());
    }
}

テストする場合にはアプリ側に<service>で設定しておかなくても良い模様。又、タイミング処理をする場合にはフラグ等を持たせておいて完了するまで処理をThread.sleepさせておく、んでデータをチェックしたりすれば良いんじゃないかなーっと

例えば一定時間で変わるLive Wallpaperのような場合だとサービスをstartさせてから一定時間変わったら表示されている現在のandroid.graphics.Bitmap?とかをチェックするとか。んまぁ要件によりきりかなと

ProviderTestCase2

package sample.test;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;

public class SampleProvider extends ContentProvider {

    private static UriMatcher matcher;
    private static final String AUTHORITIES = "sample.test.provider";
    private static final int SAMPLES = 0;

    public static final String CONTENT_URI_STRING = "content://" + AUTHORITIES + "/samples";
    public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);

    static {
        matcher = new UriMatcher(UriMatcher.NO_MATCH);
        matcher.addURI(AUTHORITIES, "samples", SAMPLES);
    }

    @Override
    public boolean onCreate() {
        return true;
    }

    @Override
    public String getType(Uri uri) {
        if (matcher.match(uri) == SAMPLES) {
            return "vnd.android.cursor.dir/" + AUTHORITIES;
        }

        return null;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        MatrixCursor csr = new MatrixCursor(new String[]{ "ID", "NAME" });

        for (int i = 0;i < 10;i++) {
            csr.addRow(new Object[]{ i, "hoge" + (i + 1) });
        }

        return csr;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }
}

な感じでqueryの検証をProviderTestCase2で行える。Service同様にテストする状態であれば<provider>定義しなくても良い模様

package sample.test;

import android.database.Cursor;
import android.test.ProviderTestCase2;

import sample.test.SampleProvider;

public class SampleProviderTestCase extends ProviderTestCase2<SampleProvider> {

    public SampleProviderTestCase() {
        super(SampleProvider.class, SampleProvider.CONTENT_URI_STRING);
    }

    public void test_query() {
        SampleProvider provider = getProvider();
        assertNotNull(provider);

        Cursor csr = provider.query(SampleProvider.CONTENT_URI, null, null, null, null);
        assertNotNull(csr);
        assertEquals(2, csr.getColumnCount());
        assertEquals(10, csr.getCount());
    }
}

な感じでProviderTestCase2を継承してgetProviderでProviderクラスの実体クラスが取得できてテスト出来る模様。ただ気になるのがコンストラクタの第2引数のプロバイダーのAuthorityがなぜ必要なのかっていう所。空にしてもテストコケないし... 謎なので分かり次第追記する

LoaderTestCase

以前に検証したので省略

ActivityUnitTestCase

アクティビティ操作をテスト出来るクラスな模様

package sample.test;


import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;

public class MainActivity extends Activity {

    private ArrayAdapter<String> adapter;

    @Override
    public void onCreate(Bundle bundle) {
        super.onCreate(bundle);
        setContentView(R.layout.main);

        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1);

        ((ListView)findViewById(R.id.listView)).setAdapter(adapter);

        ((Button)findViewById(R.id.btn)).setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                if (adapter != null) {
                    adapter.clear();
                }
            }
        });
    }

    @Override
    public void onStart() {
        super.onStart();

        fetchSamples();
    }

    public void fetchSamples() {
        if (adapter != null) {
            for (int i = 0;i < 10;i++) {
                adapter.add("test" + (i + 1));
            }
        }
    }
}

な感じで、onStartでデータを取得してListViewにバインド。んでボタンを押すとそれが消える。そういう操作をエミュレーションしてテストしてみるには

package sample.test;

import android.app.Application;
import android.content.Intent;
import android.test.ActivityUnitTestCase;
import android.widget.Button;
import android.widget.ListAdapter;
import android.widget.ListView;

public class MainActivityTestCase extends ActivityUnitTestCase<MainActivity> {

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

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

        setApplication(new MainApplication());
        startActivity(new Intent(Intent.ACTION_MAIN), null, null);
    }

    public void test_ActivityApplication() {
        MainActivity activity = getActivity();
        assertNotNull(activity);

        Application app = activity.getApplication();
        assertNotNull(app);
        assertTrue(app instanceof MainApplication);
    }

    public void test_fetchSamples1() {
        MainActivity activity = getActivity();
        getInstrumentation().callActivityOnStart(activity);

        assertNotNull(activity);

        ListView listView = (ListView)activity.findViewById(R.id.listView);
        assertNotNull(listView);

        ListAdapter adapter = listView.getAdapter();
        assertNotNull(adapter);
        assertEquals(10, adapter.getCount());
    }

    public void test_fetchSamples2() {
        MainActivity activity = getActivity();
        assertNotNull(activity);

        getInstrumentation().callActivityOnStart(activity);

        ListView listView = (ListView)activity.findViewById(R.id.listView);
        ListAdapter adapter = listView.getAdapter();
        assertNotNull(adapter);
        assertEquals(10, adapter.getCount());

        final Button btn = (Button)activity.findViewById(R.id.btn);

        activity.runOnUiThread(new Thread() {
            @Override
            public void run() {
                btn.performClick();
            }
        });
        getInstrumentation().waitForIdleSync();

        adapter = listView.getAdapter();
        assertNotNull(adapter);
        assertEquals(0, adapter.getCount());
    }
}

アクティビティは自動自動等しない模様なのでstartActivityで起動。んでsetApplicationで使用するApplicationクラスを指定出来る。でcallAcvitityOnStartでOnStartを発生させて、runOnUiThreadを使ってボタンをクリックしたり等の操作を行い、waitForIdelSyncで操作が完了するのを待つっていう感じかと

ActivityInstrumentationTestCase2

ActivityUnitTestCaseを使わずにAndroidManifest.xmlに沿った形式でAcvitityテストを行うにはこれでも出来る。ActivityUnitTestCaseで使ったAcvitityをそのままAcitvityInstrumentationTestCase2に移行させる場合には以下のような感じ

package sample.test;

import android.app.Application;
import android.test.ActivityInstrumentationTestCase2;
import android.widget.ListView;
import android.widget.ListAdapter;
import android.widget.Button;

public class MainActivityTestCase extends ActivityInstrumentationTestCase2<MainActivity> {

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

    public void test_getApplication() {
        MainActivity activity = getActivity();
        assertNotNull(activity);

        Application app = activity.getApplication();
        assertNotNull(app);
        assertTrue(app instanceof MainApplication);
    }

    public void test_fetchSamples1() {
        MainActivity activity = getActivity();
        assertNotNull(activity);

        ListView listView = (ListView)activity.findViewById(R.id.listView);
        assertNotNull(listView);

        ListAdapter adapter = listView.getAdapter();
        assertNotNull(adapter);
        assertEquals(10, adapter.getCount());
    }

    public void test_fetchSamples2() {
        MainActivity activity = getActivity();
        assertNotNull(activity);

        final Button btn = (Button)activity.findViewById(R.id.btn);

        activity.runOnUiThread(new Thread() {
            @Override
            public void run() {
                btn.performClick();
            }
        });
        getInstrumentation().waitForIdleSync();

        ListView listView = (ListView)activity.findViewById(R.id.listView);
        ListAdapter adapter = listView.getAdapter();
        assertNotNull(adapter);
        assertEquals(0, adapter.getCount());
    }
}

SingleLaunchActivityTestCase

恐らくは上記のActivityテストの場合であるとテスト毎にActivityが初期化される。でテストで実行したアクティビティの状態を維持させたい場合には一度しか初期化されない模様

package sample.test;

import android.app.Application;
import android.test.SingleLaunchActivityTestCase;
import android.widget.Button;
import android.widget.ListAdapter;
import android.widget.ListView;

public class MainActivityTestCase extends SingleLaunchActivityTestCase<MainActivity> {

    public MainActivityTestCase() {
        super("sample.test", MainActivity.class);
    }

    public void testActivity() {
        MainActivity activity = getActivity();
        assertNotNull(activity);

        Application app = activity.getApplication();
        assertNotNull(app);
        assertTrue(app instanceof MainApplication);
    }

    public void test_fetchSamples1() {
        MainActivity activity = getActivity();
        assertNotNull(activity);

        ListView listView = (ListView)activity.findViewById(R.id.listView);
        assertNotNull(listView);

        ListAdapter adapter = listView.getAdapter();
        assertNotNull(adapter);
        assertEquals(10, adapter.getCount());
    }

    public void test_fetchSamples2() {
        MainActivity activity = getActivity();
        assertNotNull(activity);

        ListView listView = (ListView)activity.findViewById(R.id.listView);

        ListAdapter adapter = listView.getAdapter();
        assertNotNull(adapter);
        assertEquals(10, adapter.getCount());

        final Button btn = (Button)activity.findViewById(R.id.btn);

        activity.runOnUiThread(new Thread() {
            @Override
            public void run() {
                btn.performClick();
            }
        });
        getInstrumentation().waitForIdleSync();

        adapter = listView.getAdapter();
        assertNotNull(adapter);
        assertEquals(0, adapter.getCount());
    }

    public void test_fetchSamples3() {
        MainActivity activity = getActivity();
        assertNotNull(activity);

        ListView listView = (ListView)activity.findViewById(R.id.listView);

        ListAdapter adapter = listView.getAdapter();
        assertNotNull(adapter);
        assertEquals(10, adapter.getCount()); // fail
    }
}

ちなみにこのテストは成功しない。test_fetchSamples3ではListAdapterを取得してデータ数をカウントしているだけだけど、test_fetchSamples2で既にAdapterをボタンクリックによりクリアされているので、以降のテストにおいてデータをチェックするとテスト毎にアクティビティが初期化されずに状態維持されたままなのでテストがfailする。テストの順序がポイントになりそうな気もする

ってな感じでAndroidコアに入ってるテストスイートなAPIは以上なんじゃないかなーって所。最近増えたのかどうかは定かじゃないけど、んまぁ以上な事を踏まえてテストする部分の所を適切なAPI選択すれば良いんじゃねーかと

nginxのrootとalias NPAPIをざっくりやってみる (2)