読者です 読者をやめる 読者になる 読者になる

遥かへのスピードランナー

シリコンバレーでAndroidアプリの開発してます。コンピュータービジョン・3D・アルゴリズム界隈にもたまに出現します。

Android MockとRoboGuiceでTDD

android

Androidアプリ開発(に限った話ではないですが)でTDDしたいと思ったときに、テスト対象クラスのフィールドをモックで差し替えたい、と思うことがしばしばあります。依存するクラスの振る舞いを固定化することで、テスト対象オブジェクトの振る舞いだけに着目したテストケースを書くことができるからです。

そんな時に、DIコンテナ上でコードを書いていると便利です。以前、少しだけSeasar2+EasyMockでテストを書いていたことがあったのですが、作成したモックオブジェクトの差し替えを、ほぼ全てSeasar2がやってくれたのでものすごく便利でした。

Android開発でもSeasar2+EasyMockくらい簡単にテストを書きたい!
ということで、

  • Android Mockでモックオブジェクトとその振る舞いを定義
  • RoboGuiceでモックオブジェクトをテスト対象クラスにインジェクト

ということをやってみました。

準備

RoboGuice導入

roboguice - Project Hosting on Google Code
RoboGuiceは、Android Framework上でDIを実現するためのフレームワークです。Google GuiceというDIライブラリを拡張してAndroidに対応させています。

導入方法については

と、それを受けた

が詳しいのでそっちを参照で。

Android Mock導入

android-mock - Project Hosting on Google Code
Android Mockは、Androidで使えるEasyMockです。
導入方法については、Androdテスト部の方が翻訳されたドキュメント

が詳しいのでそちらを参照してください。

テスト対象コード

さて準備については丸投げしたところで(おい)、いよいよDIを活用したテストコードを書いてみます。
今回テストするのは以下のようなActivityです。

TopActivity
public class TopActivity extends GuiceActivity {
	@InjectView(R.id.text)
	private TextView textView;

	@Inject
	private Person person;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.main);
		if (person.getName().equals("droid")) {
			person.setColor("green");
		} else {
			person.setColor("white");
		}
		textView.setText(person.getName());
	}
}

機能は単純で、onCreate()で、personフィールドにセットされたオブジェクトのnameが"droid"だったらsetColor("green")を呼び出し、そうでなければsetColor("white")を呼び出す、というものです。

@Injectが指定されているpersonフィールドへの代入は、Roboguiceが行ってくれていて、「Personインターフェースのinject要求に対しては、Droidクラスを差し込め」ということをApplicationクラス(とそこから呼び出されるModuleクラス)で指定しています。

MyApplication.java
public class MyApplication extends GuiceApplication {
	@Override
	protected void addApplicationModules(List<Module> modules) {
		modules.add(new MyModule());
	}
}
MyModule.java
public class MyModule extends AbstractAndroidModule {
	@Override
	protected void configure() {
		bind(Person.class).to(Droid.class);
	}
}

さて、先ほどのActivityのテストを書こうとしたときに

  • personフィールドがdroidのときと、そうでないときの両ケースをテストしたい
  • setColorに対して期待値通りの引数が指定されて呼び出されていることを確認したい

ということが実現できると、テストケースとしてはバッチリです。
これをAndroid MockとRoboGuiceで実現します。

テストコード

public class TestCase extends ActivityUnitTestCase<TopActivity> {

	final class MyMockModule extends AbstractAndroidModule {
		private Person mockPerson;

		public void setMock(Person person) {
			this.mockPerson = person;
		}

		@Override
		protected void configure() {
			bind(Person.class).toInstance(mockPerson);
		}
	}

	final class MyMockApplication extends GuiceApplication {
		private Module myModule;

		public void setMyModule(Module myModule) {
			this.myModule = myModule;
		}

		@Override
		protected void addApplicationModules(List<Module> modules) {
			if (myModule == null) {
				throw new IllegalArgumentException("Please call setMyModule before running the tests!");
			}
			modules.add(myModule);
		}

		MyMockApplication(Context context) {
			super();
			attachBaseContext(context);
		}
	}

	public TestCase() {
		super(TopActivity.class);
	}

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

	}

	@MediumTest
	@UsesMocks(Droid.class)
	public void testMockDroid1() {

		// create a mock and learn it's behavior
		Droid mockDroid = AndroidMock.createMock(Droid.class);
		AndroidMock.expect(mockDroid.getName()).andStubReturn("mock");
		mockDroid.setColor("white");
		AndroidMock.replay(mockDroid);

		// set up mock application object
		Context context = getInstrumentation().getTargetContext();
		MyMockApplication application = new MyMockApplication(context);
		MyMockModule myMockModule = new MyMockModule();
		myMockModule.setMock(mockDroid);
		application.setMyModule(myMockModule);
		setApplication(application);

		// start activity
		Intent intent = new Intent(context, TopActivity.class);
		TopActivity activity = startActivity(intent, null, null);
		TextView textView = (TextView) activity
		        .findViewById(com.polysfactory.roboguice_and_mock.R.id.text);

		// verify
		assertEquals("mock", textView.getText());
		AndroidMock.verify(mockDroid);
	}
	
	@MediumTest
	@UsesMocks(Droid.class)
	public void testMockDroid2() {

		// create a mock and learn it's behavior
		Droid mockDroid = AndroidMock.createMock(Droid.class);
		AndroidMock.expect(mockDroid.getName()).andStubReturn("droid");
		mockDroid.setColor("green");
		AndroidMock.replay(mockDroid);

		// set up mock application object
		Context context = getInstrumentation().getTargetContext();
		MyMockApplication application = new MyMockApplication(context);
		MyMockModule myMockModule = new MyMockModule();
		myMockModule.setMock(mockDroid);
		application.setMyModule(myMockModule);
		setApplication(application);

		// start activity
		Intent intent = new Intent(context, TopActivity.class);
		TopActivity activity = startActivity(intent, null, null);
		TextView textView = (TextView) activity
		        .findViewById(com.polysfactory.roboguice_and_mock.R.id.text);

		// verify
		assertEquals("droid", textView.getText());
		AndroidMock.verify(mockDroid);
	}

}

肝となるのは、

	final class MyMockModule extends AbstractAndroidModule {
                ...
		@Override
		protected void configure() {
			bind(Person.class).toInstance(mockPerson);
		}
	}

の部分です。このbindによって、Personクラスの@Inject要求に対しては、モックオブジェクトがセットされるようになります。

モックの振る舞いは、各テストメソッドの

		Droid mockDroid = AndroidMock.createMock(Droid.class);
		AndroidMock.expect(mockDroid.getName()).andStubReturn("droid");
		mockDroid.setColor("green");
		AndroidMock.replay(mockDroid);
                ...
                AndroidMock.verify(mockDroid);

の部分で指定しており、モックに対する動作を検証することもできます。

仮にDI(RoboGuice)を使わずに、personフィールドをonCreateの中でnewしていたら、このように簡単にテストを書くことはできません。personフィールドに何がセットされるかについては、newしたクラスの中身が完全に把握していないと分からないからです。

課題

@InjectViewや@InjectExtraのフィールドにモックインジェクションが出来ない

今回のテストでは、@Injectを指定したフィールドに対してモックをセットしています。
@Injectによるインジェクションは、実体に対するマッピングを自分でカスタマイズすることが出来るので、モックの差し替えが容易でしたが、@InjectViewや@InjectExtraなどのRoboguiceが提供するその他のインジェクションタイプに対しては、モックの差し替えはできなそうでした。(Roboguice自体に、モックと差し替えられるような拡張ポイントを用意してあげる必要があると思います。)
実際の開発では、@InjectViewや@InjectExtraもかなり便利なので、是非ともモックに差し替えてテストを書いてみたいところです。

RoboGuiceの性能

上記の方法は元々のアプリケーションがRoboGuiceで作られていることが前提となっています。
RoboGuiceの性能に関しては、Androidで動かすことを前提に、かなり気を使ったコードを書いているように見えますが、DIの宿命としてリフレクションを多用していますし、性能は気になります。
ただこれも、「推測するな、計測せよ」の原則に則ってきちんと計測すべきですね^^;

上記について詳しい方、いらっしゃいましたらアドバイス下さい!!

まとめ

繰り返しになりますが、

  • Android Mockを使うことでモックの振る舞いを固定化し、テスト中のモックの動作を検証する
  • RoboGuiceを使うことでモックをテスト対象クラスにインジェクション可能にする

これだけで格段にテストしやすいコードになると思います。

androidのテストメソッドはまだ未成熟で、色々なコミュニティが色々なライブラリを出しているのでこれが最善、とは一概には言えないのがもどかしいんですが、今まで試した方法の中では割と使える部類のテスト方法だと思ってます。