GroovyなAndroidテスト
             8.9.2012
  Takahiro Yoshimura (@alterakey)
こんな人


埼玉で活動しているアーキテクト

Webサービスやスマートフォンアプリなどを作っています

Androidは…まだそこそこ (汗)
テスト、間に合ってます?

標準添付のAndroid Testing Framework…

実機で動かすタイプ

しかし実機で動かしていると遅い!
→ABS、Guavaなどを入れると簡単に数分台のビルド時間
→ProGuardなどでShrinkすれば良いが…

高速化は本質的に困難
そこでGroovyですよ!
背景
Groovyって何?

 JVMで動作する動的言語の一種

 Javaと違って型や制約にうるさくない
 →privateメソッドだろうが何だろうが呼べる
 →Assertionが親切!
 →テストに最適

 Javaで書かれたライブラリなども利用可能
Androidでは?


残念ながら、Android開発には簡単に使用できない
Discobotなどの成果もあることにはあるが…

しかしJVMで動作させても良いテストには有効
…は?JVM?

JVMでAndroidなんか動かないだろうが…

android.jarを追加してもシステムクラスにはほぼ触れない

java.lang.RuntimeException: Stub!

Androidシステムクラスをテストしたいが…

                                    … orz
Robolectric!

AndroidアプリをJVMでテストするためのフレームワーク

Androidシステムクラスをことごとく置き換えてテスト可能に

 ActivityやServiceといったものをそのままnew…!

 SQLite系ですらエミュレート

 Fragmentは…
 Support Packageを使っていればOK
だが面倒だ

GroovyやRobolectricを落としてきて、別途インストールしたり

EclipseではJUnitプロジェクトを立てていろいろ設定を…(ry

Antではcustom_rules.xmlを作成していろいろルールを…(ry

                                    … orz
もったいない…
ということでAndroid Ant Runnerを作りました
→ http://github.com/taky/android-runner (BSD)

GroovyやSpockを始めとするテストツールキットを
Antベースのプロジェクトから簡単に使えるようにするパッケージ
(Android SDK r20以上)

Eclipseは?IDEAは?Mavenは?Gradleは? …すみませんが (ry

Unix系限定です… ごめんなさい m(_ _)m
そもそも



なんでテストテスト言っているの?
テストは設計手段の一つ

詳細仕様を先に定義、コードはそれに合わせる形に

クラスの「詳細設計書」としてのユニットテスト

振舞駆動開発(BDD)


→ プロジェクトに参加しやすく、リファクタリングもしやすく
軽くデモ
コマンドライン+テキストエディタ

導入∼結合テストまで

テキストビューア
Intentを受け取ってContentResolverに渡してTextViewに表示

YouTubeにあります…
http://www.youtube.com/watch?v=nrT50tpIjZE
では詳細に



導入
導入

Githubからcloneしてきてbootstrapするだけ!

 詳細はREADMEに記載

これでGroovy、Spock、Robolectricその他諸々全て手に入る!

すぐにでもユニットテストを書ける状態に!
Activityの設計

MainActivity: メイン画面

「This is a text viewer.」と書いてあること

まずはテストケースから
Activityについては結合テストにも一応書いておく
※実機で問題があれば把握したいため

ここからがほぼGroovy!
Activityの設計

integration/.../MainActivityTest.java:

public class MainActivityTest extends ActivityInstrumentationTestCase2<...> {
  ...
  public void test_000() {
      getActivity();
  }
}
Activityの設計
unit/.../MainActivityTest.groovy:
import spock.lang.*
@RunWith(TestRunner)
class MainActivityTest extends Specification {
   @Test def test_000() {
      def o = new MainActivity()
      o.onCreate(null)
      when:
      def tv = o.findViewById(R.id.view)
      then:
      tv.getText() == This is a text viewer.
  }
}
Activityの設計
Spockで仕様が読みやすくなる!

I. Groovyのみ                        II. Groovy+Spock
// 初期状態                            setup:           // (省略可)
def o = new MainActivity()         def o = new MainActivity()
o.onCreate(null)                   o.onCreate(null)
// こうした場合                          when:
def tv = o.findViewById(...)        def tv = o.findViewById(...)
// こうあるべき                          then:
assertEquals(tv.getText(), ... )   tv.getText() == ...
Activityの設計

RobolectricはJUnit 4.x、Android JUnitはJUnit 3.x
→4.xは @Test Annotationで、3.xは名前でテストを特定

@RunWith(TestRunner)
→Robolectric標準を多少カスタムしたテストランナー
→AARでRobolectricを使うにはこのAnnotationが必要

テストランナーは複数指定できない
→テストフレームワーク間で取り合いになりやすい orz
Activityの設計
Robolectricはパラメタライズドテストを扱えない

つまり
「Spockのexpectを使って似通ったテストをまとめて簡潔に!」

                                 → 今はまだムリ orz

単純にRobolectricTestRunnerの問題なので、Robolectricを
ルールなどの形で再実装できれば解決するかも

もちろんRobolectricを使わないテストならパラメタライズ可能
しくじったよ!

実装していないので当然

ここから実装を書いて成功させる

 res/layout/main.xmlのTextViewに:

   IDを振ってメッセージを入れる
成功だ!次は?



ViewActivity: 表示画面

Intent.ACTION_VIEWを受けとって中身を表示できること
Activityの設計



まず表示できること
Activityの設計

integration/.../ViewActivityTest.java:

public class ViewActivityTest extends ActivityInstrumentationTestCase2<...> {
  ...
  public void test_000() {
      getActivity();
  }
}
で、実際の挙動は…??

Intent.ACTION_VIEWを受けとって中身を表示できること

しかし、どうやってロードしたことを確かめる?

Intentはどこからどう飛ばす?

テストデータはどう準備すれば?
テスト可能性を考えて…

ここではTextLoaderとしてロード処理を分離
→テスト時はハリボテ(スタブクラス)を噛ませる

Activityは単にUIに専念させる
→いろいろやっているクラスはテストが難しくなりがち
→テストしやすくするためには設計の工夫が必要!

IntentはRobolectricで飛ばしたことにできる!
→Robolectricのテスト用クラスに直接渡す
Activityの設計
unit/.../ViewActivityTest.groovy:

@RunWith(TestRunner)
class ViewActivityTest extends Specification {
   @Test def test_000() {
      # TBD: LOAD HERE, with TextLoader!
      def o = new ViewActivity()
      o.onCreate(null)
      when:
      def tv = o.findViewById(R.id.view)
      then:
      tv.getText() == File read!
   }
}
Activityの設計

unit/.../ViewActivityTest.groovy:

@Test def test_000() {
  ...
  def o = new ViewActivity()
  def intent = new Intent(Intent.ACTION_VIEW)
  intent.setData(Uri.parse( content://test-1 ))
  Robolectric.shadowOf(o).setIntent(intent)
  o.onCreate(null)
  ...
スタブはどう作れば…


Groovyならスタブの作成も楽々…なはずなのだけど

Javaから呼び出されるクラスはGroovyではモックできない!

Mockitoを使えばOK
→Android Ant Runnerに同梱してある
で、スタブはどう作れば…

def o = mock(XXX)とした後に…

when(o.method1()).thenReturn( foo )
when(o.method2(anyObject()).thenReturn( bar )
when(o.method2(eq(null))).thenThrow(new NullPointerException())
→ o.method1()     foo
  o.method2(Object arg)         bar
  o.method2(null)     NullPointerException(例外)
で、スタブはどう作れば…


簡単なものなら一行で書きたい! …書けますよ?

def o = mock(XXX)
when(o.method()).thenReturn( foo )

def o = when(mock(XXX).method()).thenReturn( foo ).getMock()
Activityの設計
unit/.../ViewActivityTest.groovy

@Test def test_000() {
  ...
  # TBD: LOAD HERE, with TextLoader!
  def loader = when(mock(TextLoader).read()).thenReturn( File read! ).getMock()
  def loader2 = when(mock(TextLoader).read()).thenReturn( ).getMock()
  def builder = mock(TextLoader.Builder)
  when(builder.build(anyObject(), anyObject())).thenReturn(loader)
  when(builder.build(anyObject(), eq(null))).thenReturn(loader2)
  def o = new ViewActivity()
  ...
で、スタブはどう渡せば…

unit/.../ViewActivityTest.groovy

def test_000() {
  ...
  def o = new ViewActivity(builder)
  ...


                                      …!!
で、スタブはどう渡せば…


Dependency Injection (DI)
→パラメータなどとしてコラボレータを外部から導入する手法
→クラス間の結合度を低下させることができる

今回はコンストラクタ(ctor)へ導入
→空のctorも作っておく必要があることに注意
で、スタブはどう渡せば…

Guiceなどを使うとより手軽に適用可能…なはずなのだが
そもそもAndroidではInversion of Controlされている環境
(i.e. Activityなどはシステムが勝手に作るもの)
→これが空のctorを要求される理由

いつinjectorを初期化するのかが問題に
→RoboGuiceでは専用クラスを継承させている(!!)
で、スタブはどう渡せば…



Guiceはともかく、RoboGuiceは
自動テストの役にはあまり立たなそうな…?
実装



テスト失敗、実装へ…

TextLoaderは骨組だけ
注意


ViewActivityの画面構成についてはテストしていない

結合テストで「最低限表示できる」レベルを担保
→まだ動かしていないけど…
次は?


TextLoader: テキストローダ
.read() → あらかじめ指定されたURIの内容をStringで

これもテストケースから(ry
ロジックの設計

unit/.../TextLoaderTest.groovy:

@RunWith(TestRunner)
class TextLoaderTest extends Specification {
   @Test def test_000() {
       when:
       def o = new TextLoader(new Activity(), Uri.parse( content://test-1 ))
       then:
       o.read() == Test file 1
   }
   ...
ロジックの設計

unit/.../TextLoaderTest.groovy:

  ...
  @Test def test_001() {
      when:
      def o = new TextLoader(new Activity(), null)
      then:
      o.read() ==
  }
  ...
テスト失敗、実装を…
書いたはいいよ?



でもContentResolverなんてどうやってテストする?
Robolectric!


独自クラスで置換することもできる
→継承している必要すらない

もちろんContentResolverも例外では…ないッ!
ということで

ShadowContentResolverクラスを作成、
ContentResolverとして振る舞わせることに。

SCR.openInputStream(Uri):

   content://test-1 → Test file 1 と読めるInputStream

  その他 → null

TestRunner.bindShadowClassesに追記。
ContentResolverの影

unit/.../TestRunner.java:

public class TestRunner extends RobolectricTestRunner {
  ...
  protected void bindShadowClasses() {
      super.bindShadowClasses();
      Robolectric.bindShadowClass(ShadowSimpleAdapter.class);
      Robolectric.bindShadowClass(ShadowContentResolver.class);
  }
}
ContentResolverの影


unit/.../ShadowContentResolver.java

長いので割愛します m(_ _)m
YouTubeのデモを参照して下さい…
http://www.youtube.com/watch?v=nrT50tpIjZE
成功!次は!



いよいよ実機で
orz
ぐぬぬぬぬ…

MainActivityはいい…
しかしViewActivityがピヨピヨしていることが判明

なぜ?→AndroidManifest.xmlに宣言されていないらしい

Error in test_000:
java.lang.RuntimeException: unable to resolve activity for Intent { action=...
  flg=... cmp=... /.ViewActivity }
  at ...
  at ...
ぐぬぬぬぬ… (2)



AndroidManifest.xmlにViewActivityの宣言を追加

ついでにSDKバージョンの宣言も
成功!



すっきり!
まとめ

Android JUnitは実機でテストできるが、ビルドが非常に遅い

GroovyはJVMで動作する簡潔・動的な言語
→静的な型に縛られない書き方ができるのでテスト向き
→実装の関係上、Android開発で使用するのはまだ難しい…

実機で動かさないテストならGroovyで十分にできる
→TDD/BDDが現実的に!
まとめ


JVM上でAndroidシステムクラスは本来テストできないが、
Robolectricを使うとテスト可能になる

Activityのテストを行なう場合にはユニットテストだけでなく、
Android JUnitによる結合テストも併用すると良い
まとめ


Spockを併用するとテスト仕様をより読みやすく記述できる

Robolectric自体はまだパラメタライズドテストを扱えない
→expect+whereのような華麗なテストはできない…
→ランナーの問題なのでルールなどの形にできれば解決するかも
→もちろんRobolectricを絡ませなければパラメタライズ可能!
まとめ

Javaから呼ばれるクラスはGroovyでモックできないので、
Mockitoなどのモックフレームワークを併用する必要がある

Robolectricを使うと任意のクラスをAndroidシステムクラスとし
て振る舞わせることができる

AndroidのクラスをモックするにはRobolectric
自前のクラスをモックするにはモックフレームワーク
まとめ



Android Ant Runnerを使うとテスト環境を簡単に整備できる

http://github.com/taky/android-runner (BSD)
ご静聴ありがとうございました。




Credits: Music by "FIGHTER X” – "Time to die" in album “REBOOT2,” © 2012 chipcon.org, used under the Creative Commons 3.0 Attribution-NonCommercial-NoDerivs Unported license: http://creativecommons.org/licenses/by-nc-nd/3.0/

GroovyなAndroidテスト #atest_hack