スキップしてメイン コンテンツに移動

onCreate実行前にActivity内メソッドの戻り値を差し替てテストしたい。Mockitoで

-------------追記:2015/01/12-------------
書き直しました→Mockitoを使ってActivityの分岐のテスト ~ takaiwa.net
-----------------------------------------
良いやり方ではないと思うんですけど、力業で解決したので載せておきます。
やりたい事はこの質問者さんとだいたい同じだと思います(Activityの話かどうかわかりませんが)
・onCreateメソッドのテスト - Google グループ
https://groups.google.com/forum/#!msg/android-group-japan/0H_wA7IYJ6k/gdqwloJnjR8J
setUp()メソッドをオーバーライドしてgetActivity()を記述しても、その時点でonCreateが呼ばれてしまうんですね。何でonCreate前にそんな事をしたいのかと言うと、上記の質問および下記のサンプルソースから察していただければと思うんですけど、
[テスト対象Activity]
public class MainActivity extends Activity {

    private ServerAPI server_api;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if(true == isLoggedIn()) {
            ((TextView)findViewById(R.id.textView1)).setText("Logged In");
        }
        else {
            ((TextView)findViewById(R.id.textView1)).setText("Not logged in");
        }
    }

    public boolean isLoggedIn() {
        server_api = new ServerAPI();
        // falseが返る
        return server_api.isConnected();
    }
}

isLoggedIn()はサーバへアクセスするメソッドという風に考えてもらって、戻り値がtrueなら、ログイン済みという旨をTextViewへ表示。falseならログイン未という旨を表示するActivityです。テストの度にサーバへぼこぼこアクセスするのも気が引けますし(※1)、単体テストにログイン/ログアウトの手順を踏むのも違う気が。JUnitのテストプロジェクトから自由にisLoggedIn()の戻り値を書き換えてonCreate内の分岐をテストしたいわけです。が、先ほど申しましたようにgetActivity()時点でonCreate()まで実行済みになってしまう。他に良いAPIがあるのでしょうか?


テストされてる方どういう風にテストコードを書いてらっしゃるのか、ググれどググれどよくわからないので、コメント欄にもでご教授いただければ幸いに存じます。まあそもそも、こんなコードを書くなということなのかもしれませんが、レガシーコードなら普通にありそうですよね?

ということで、試行錯誤してみました。

ActivityのテストだとActivityInstrumentationTestCase2とかSingleLaunchActivityTestCaseやActivityUnitTestCaseになるかと思いますが、ActivityUnitTestCaseでライフサイクルを手動で呼び出せるということで、ソースを読んでstartActivityに注目してみました。

(ソースの場所はダウンロードしていれば、android-sdk/sources/android-16/android/test/ActivityUnitTestCase.java にあります)

このstartActivity内のnewActivityした直後でMockitoのspyを適用すれば、onCreateが実行される前にメソッドの戻り値を差し替えられるのではないかと。

(Mockitoやspyは前回の記事で
http://www.takaiwa.net/2013/01/androidmockito.html

というわけでテストプロジェクトでActivityUnitTestCaseを継承して、startActivityをOverrideするにあたり、startActivityのソースが、
protected T startActivity(Intent intent, Bundle savedInstanceState,
とあって、戻り値の型がTなのですが、これはActivityUnitTestCaseを継承した時のジェネリックの型に合せれば良いです。この場合、
    @Override
    protected MainActivity startActivity(Intent intent,
            Bundle savedInstanceState, Object lastNonConfigurationInstance) {
テスト対象であるMainActivityです。まあEclipseならstartActivityと打ってCtrl + [スペース]のコードアシストを使えばOverrideアノテーションと共に勝手に記述してくれます。

あと、テストプロジェクト上でstartActivityが動くようにいろいろやらないといけないのですが....MockParentがprivateクラスなので、ごっそりAndroidソースから拝借します。とてもエレガントなやり方ではないですね(;^_^A

テスト自体は、spyしたオブジェクトから、isLoggedIn()の戻りがtrueになってることと、TextViewのテキストが、ログイン済みである”Logged In”が表示されることを確認します。あと、細かい点はコメントに書いてます。

package net.takaiwa.mockitosample.test;

import net.takaiwa.mockitosample.R;
import net.takaiwa.mockitosample.MainActivity;
import android.os.Bundle;
import android.os.IBinder;
import android.test.ActivityUnitTestCase;
import android.view.Window;
import android.widget.TextView;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.spy;
import android.test.mock.MockApplication;
import android.util.Log;
import android.content.ComponentName;

public class MainActivityTest extends ActivityUnitTestCase<MainActivity> {

    private static final String TAG = MainActivityTest.class.getSimpleName();

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

    private TextView text_view;
    private MainActivity spy_activity;
    // --------- startActivity用 ---------------------
    private Context mActivityContext;
    private Class<MainActivity> mActivityClass = MainActivity.class;
    private boolean mCreated = false;
    private boolean mAttached = false;
    private Application mApplication = null;
    private MockParent mMockParent;
    // -----------------------------------------------

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        mActivityContext = getInstrumentation().getTargetContext();
        // Ativity起動(Overrideしたメソッド内部でspyを介してActivityオブジェクトを取得して差し替え)
        startActivity(new Intent(), null, null);
    }

    public void testActivity() throws Exception {
        // 対象のメソッド戻り値を確認しておく
        assertTrue(spy_activity.isLoggedIn());

        text_view = (TextView)spy_activity.findViewById(R.id.textView1);
        // 画面にログイン済みの旨が表示されていることを確認
        assertEquals("Logged In", text_view.getText());
    }

    @Override
    protected MainActivity startActivity(Intent intent,
            Bundle savedInstanceState, Object lastNonConfigurationInstance) {
        assertFalse("Activity already created", mCreated);

        if (!mAttached) {
            assertNotNull(mActivityClass);
            setActivity(null);
            MainActivity newActivity = null;
            try {
                IBinder token = null;
                if (mApplication == null) {
                    // ↓そのまま使うよう書き換え↓
                    mApplication = new MockApplication();
                    setApplication(mApplication);

                }
                ComponentName cn = new ComponentName(mActivityClass.getPackage().getName(),
                        mActivityClass.getName());
                intent.setComponent(cn);
                ActivityInfo info = new ActivityInfo();
                CharSequence title = mActivityClass.getName();
                mMockParent = new MockParent();
                String id = null;

                newActivity = (MainActivity) getInstrumentation().newActivity(mActivityClass, mActivityContext,
                        token, mApplication, intent, info, title, mMockParent, id,
                        lastNonConfigurationInstance);

                ////////////////////////////
                // ↓↓ ここでActivityオブジェクト受け取って書き換え ↓↓
                this.spy_activity = spy(newActivity);
                when(this.spy_activity.isLoggedIn()).thenReturn(true);
                ////////////////////////////

            } catch (Exception e) {
                assertNotNull(newActivity);
            }

//            assertNotNull(newActivity);
//            setActivity(newActivity);
            assertNotNull(this.spy_activity);
            // 差し替え
            setActivity(this.spy_activity);

            mAttached = true;
        }

        MainActivity result = getActivity();
        if (result != null) {
            //ここでonCreateが実行される
            getInstrumentation().callActivityOnCreate(getActivity(), savedInstanceState);
            mCreated = true;
        }
        return result;
    }

    // Androidソースからごっそり拝借
    private static class MockParent extends Activity {

        public int mRequestedOrientation = 0;
        public Intent mStartedActivityIntent = null;
        public int mStartedActivityRequest = -1;
        public boolean mFinished = false;
        public int mFinishedActivityRequest = -1;

        /**
         * Implementing in the parent allows the user to call this function on the tested activity.
         */
        @Override
        public void setRequestedOrientation(int requestedOrientation) {
            mRequestedOrientation = requestedOrientation;
        }

        /**
         * Implementing in the parent allows the user to call this function on the tested activity.
         */
        @Override
        public int getRequestedOrientation() {
            return mRequestedOrientation;
        }

        /**
         * By returning null here, we inhibit the creation of any "container" for the window.
         */
        @Override
        public Window getWindow() {
            return null;
        }

        /**
         * By defining this in the parent, we allow the tested activity to call
         * <ul>
         * <li>{@link android.app.Activity#startActivity(Intent)}</li>
         * <li>{@link android.app.Activity#startActivityForResult(Intent, int)}</li>
         * </ul>
         */
        @Override
        public void startActivityFromChild(Activity child, Intent intent, int requestCode) {
            mStartedActivityIntent = intent;
            mStartedActivityRequest = requestCode;
        }

        /**
         * By defining this in the parent, we allow the tested activity to call
         * <ul>
         * <li>{@link android.app.Activity#finish()}</li>
         * <li>{@link android.app.Activity#finishFromChild(Activity child)}</li>
         * </ul>
         */
        @Override
        public void finishFromChild(Activity child) {
            mFinished = true;
        }

        /**
         * By defining this in the parent, we allow the tested activity to call
         * <ul>
         * <li>{@link android.app.Activity#finishActivity(int requestCode)}</li>
         * </ul>
         */
        @Override
        public void finishActivityFromChild(Activity child, int requestCode) {
            mFinished = true;
            mFinishedActivityRequest = requestCode;
        }
    }
}

実行されてない所とか省けば、もっとスマートになるかと思いますが、1つ2つ確認するのに長いコードになってしまいました(;^_^A これでテスト対象のActivityのメソッドの戻り値を操れます。spyの適用が88行目でisLoggedIn()戻り値の書き換えが89行目です。それを、100行目のsetActivityで差し替えてます。めでたしめでたし。


-- 追記:2013/01/13 --------------------
70行目のmApplication = new MockApplication();のようにしていると、独自にApplicationクラスを継承したクラスをAndroidManifestに定義している場合、Activity上でMyApplication my_application = (MyApplication)this.getApplication();のように取得しようとすると、ClassCastExceptionが発生します。なので、このような場合、startActivity実行前に、
        mApplication = Instrumentation.newApplication(MyApplication.class, mActivityContext);
        mApplication.onCreate();
        setApplication(mApplication);
とすると良いんじゃないかと思います。


※1サーバへぽこぽこアクセスさせないよう差し替えると記述してますが、89行目when(this.spy_activity.isLoggedIn()).thenReturn(true);で戻り値を変更してもonCreate時にそのメソッドの中身は実行されるので、結果としてサーバへはアクセスしてしまいます。実行させないためには、例えば、MainAcctivityの19行目を削除してprivate ServerAPI server_api = new ServerAPI();とし、テストコード89行目の下あたりでそのprivateフィールドを置き換える(ServerAPIで継承してもので)ような工夫が必要となります。そこまで配慮するなら、別のテストライブラリも検討した方がいいかもしれません。詳しくは調べてませんが
-------------------------- 追記終り ---



まあ今回のMainActivityのようなものであれば、onCreateの書き方としてTextViewのsetText()の部分はメソッドとして書き出すのが良いでしょうね。パラメータを渡せるようにして。

    public void setTextView(boolean logged_in) {
        if(true == logged_in) {
            ((TextView)findViewById(R.id.textView1)).setText("Logged In");
        }
        else {
            ((TextView)findViewById(R.id.textView1)).setText("Not logged in");
        }
    }

ここだけテストできますが、バージョンアップの度にテストすることを考えると結局サーバ部分の処理がネックになると思います。onStartに記述できるならそっちへ書いてしまえば、ActivityUnitTestCaseだとonCreateまでで止められるので、メソッドのみのテストもできますが、ライフサイクルとしては(onCreateは生成時のみで、onStartは他のActivityから戻ってきた場合も実行されるなど)アプローチとして問題ですね。

となると、やっぱりテストをするのに、onCreateが実行される前にメソッドを差し替える必要があるわけですが、

そもそもそんな状態はライサイクル的に無く、扱っていいのか疑問な部分ではあるため、テストのやり方としてもうちょっと考えないといけない気がします。
まだまだ修行が必要ですね、テスト駆動開発.....。

コメント

このブログの人気の投稿

Javaでprivateなfieldやmethodにアクセスする

JUnitでテストしてると、privateなフィールドにアクセスして、値を参照したりセットしたりしたくなるわけですが、よく使うのでメモしておきます。 例えば、次のような対象のクラスがあるとします。 public class ParentClass { private String hoge = "ParentClass!!"; public void dispMsg() { System.out.println("dispMsg:" + hoge); } private void privateDispMsg(String msg) { System.out.println("dispMsg:" + msg); } }

GolangでWindows GUIアプリケーション

GUIアプリ作成の前提 社内ツールとしてexeで配布 開発環境はGoLandを使う 社内ツールとしてexeを配布ということであれば、Visual StudioでC#による開発だと思います。しかしながら、Go言語を習得したいのと、GoLandの補完機能が便利で、Android Studio使っていたこともあり、とっつきやすいという点からGo縛りでGUIアプリケーションを考えたいと思います。 lxn/walk Windows application library kit for Go. Windows向けしか考えていないので、まずこのライブラリなのですが、ボタンやコンボボックスが思ったように並ばなかったり、手軽にイメージボタンを配置したりなど、簡単にレイアウトを変更できない課題に直面しました。レイアウト作成だけで時間を取られてしまいます。そう言えば、Androidアプリ開発のときは、XMLでデザイン部分を切り離してたのを思い出して、今回の調査の運びとなりました。 fyne-io/fyne Cross platform native GUIs designed for Go based on Material Design. Supports: Linux, macOS, Windows, BSD, iOS and Android. walkと比べると、クロスプラットフォームで作成できるのですが、こちらもコードの中にデザインを書いていく形でした。 therecipe/qt therecipe/qt allows you to write Qt applications entirely in Go or JavaScript. Qt Creatorなるものがあるようで、デザイン部分を切り離せる印象はあるものの、最新のPCでもビルドに時間がかかるようで、学習コストもかかるという記事を見かけて断念しました。 どうしたものかとツイートしましたら、下記のようにご助言をいただいた。 同じ課題に対し色々試した結果、PWAのフルキャッシュドに落ち着いた。中身はGo+WASMで。 https://t.co/e60whDTV16 — のぼのぼ📡 (@nobonobo) July 21, 2020 PWA 早速調べてみました。Googleが進めているプロジェクトで、ネイティブアプリのよ

Eclipseの高速化メモ

Eclipseが重いと一言に言っても、いろいろな工程での話があると思いますが、過去記事のなども含めてこの記事にピックアップしておきたいと思います。以下はWindows環境での話です。