この広告は、90日以上更新していないブログに表示しています。
Androidアプリ開発(に限った話ではないですが)でTDDしたいと思ったときに、テスト対象クラスのフィールドをモックで差し替えたい、と思うことがしばしばあります。依存するクラスの振る舞いを固定化することで、テスト対象オブジェクトの振る舞いだけに着目したテストケースを書くことができるからです。
そんな時に、DIコンテナ上でコードを書いていると便利です。以前、少しだけSeasar2+EasyMockでテストを書いていたことがあったのですが、作成したモックオブジェクトの差し替えを、ほぼ全てSeasar2がやってくれたのでものすごく便利でした。
Android開発でもSeasar2+EasyMockくらい簡単にテストを書きたい!
ということで、
ということをやってみました。
roboguice - Project Hosting on Google Code
RoboGuiceは、Android Framework上でDIを実現するためのフレームワークです。GoogleGuiceというDIライブラリを拡張してAndroidに対応させています。
導入方法については
と、それを受けた
が詳しいのでそっちを参照で。
android-mock - Project Hosting on Google Code
Android Mockは、Androidで使えるEasyMockです。
導入方法については、Androdテスト部の方が翻訳されたドキュメント
が詳しいのでそちらを参照してください。
さて準備については丸投げしたところで(おい)、いよいよDIを活用したテストコードを書いてみます。
今回テストするのは以下のようなActivityです。
publicclass TopActivityextends GuiceActivity {@InjectView(R.id.text)private TextView textView;@Injectprivate Person person;@Overridepublicvoid 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クラス)で指定しています。
publicclass MyApplicationextends GuiceApplication {@Overrideprotectedvoid addApplicationModules(List<Module> modules) {modules.add(new MyModule());}}
publicclass MyModuleextends AbstractAndroidModule {@Overrideprotectedvoid configure() {bind(Person.class).to(Droid.class);}}
さて、先ほどのActivityのテストを書こうとしたときに
ということが実現できると、テストケースとしてはバッチリです。
これをAndroid MockとRoboGuiceで実現します。
publicclass TestCaseextends ActivityUnitTestCase<TopActivity> {finalclass MyMockModuleextends AbstractAndroidModule {private Person mockPerson;publicvoid setMock(Person person) {this.mockPerson = person;}@Overrideprotectedvoid configure() {bind(Person.class).toInstance(mockPerson);}}finalclass MyMockApplicationextends GuiceApplication {private Module myModule;publicvoid setMyModule(Module myModule) {this.myModule = myModule;}@Overrideprotectedvoid addApplicationModules(List<Module> modules) {if (myModule ==null) {thrownew IllegalArgumentException("Please call setMyModule before running the tests!");}modules.add(myModule);}MyMockApplication(Context context) {super();attachBaseContext(context);}}public TestCase() {super(TopActivity.class);}@Overrideprotectedvoid setUp()throws Exception {super.setUp();}@MediumTest@UsesMocks(Droid.class)publicvoid testMockDroid1() {// create a mock and learn it's behaviorDroid mockDroid = AndroidMock.createMock(Droid.class);AndroidMock.expect(mockDroid.getName()).andStubReturn("mock");mockDroid.setColor("white");AndroidMock.replay(mockDroid);// set up mock application objectContext context = getInstrumentation().getTargetContext();MyMockApplication application =new MyMockApplication(context);MyMockModule myMockModule =new MyMockModule();myMockModule.setMock(mockDroid);application.setMyModule(myMockModule);setApplication(application);// start activityIntent 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);// verifyassertEquals("mock", textView.getText());AndroidMock.verify(mockDroid);}@MediumTest@UsesMocks(Droid.class)publicvoid testMockDroid2() {// create a mock and learn it's behaviorDroid mockDroid = AndroidMock.createMock(Droid.class);AndroidMock.expect(mockDroid.getName()).andStubReturn("droid");mockDroid.setColor("green");AndroidMock.replay(mockDroid);// set up mock application objectContext context = getInstrumentation().getTargetContext();MyMockApplication application =new MyMockApplication(context);MyMockModule myMockModule =new MyMockModule();myMockModule.setMock(mockDroid);application.setMyModule(myMockModule);setApplication(application);// start activityIntent 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);// verifyassertEquals("droid", textView.getText());AndroidMock.verify(mockDroid);}}
肝となるのは、
finalclass MyMockModuleextends AbstractAndroidModule {...@Overrideprotectedvoid 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したクラスの中身が完全に把握していないと分からないからです。
今回のテストでは、@Injectを指定したフィールドに対してモックをセットしています。
@Injectによるインジェクションは、実体に対するマッピングを自分でカスタマイズすることが出来るので、モックの差し替えが容易でしたが、@InjectViewや@InjectExtraなどのRoboguiceが提供するその他のインジェクションタイプに対しては、モックの差し替えはできなそうでした。(Roboguice自体に、モックと差し替えられるような拡張ポイントを用意してあげる必要があると思います。)
実際の開発では、@InjectViewや@InjectExtraもかなり便利なので、是非ともモックに差し替えてテストを書いてみたいところです。
上記の方法は元々のアプリケーションがRoboGuiceで作られていることが前提となっています。
RoboGuiceの性能に関しては、Androidで動かすことを前提に、かなり気を使ったコードを書いているように見えますが、DIの宿命としてリフレクションを多用していますし、性能は気になります。
ただこれも、「推測するな、計測せよ」の原則に則ってきちんと計測すべきですね^^;
上記について詳しい方、いらっしゃいましたらアドバイス下さい!!
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。