NHN Cloud NHN Cloud Meetup!

[Android] ユニットテスト・フレームワーク Robolectric

はじめに

今回は、Android テスティングフレームワークRobolectricについて紹介します。
まずは簡単にAndroidのテスト種類を調べて、その後、Robolectricについて紹介していきたいと思います。

Android Test

Androidで開発してみると、自然と「テスト」に目が行くようになります。Androidのテストは次の方法に大きく分けられます。

  • ユニットテスト(Unit Test)
  • インストゥルメント化テスト(Instrumentation Test)

ユニットテストは、文字通り「単体テスト」を意味します。テスト駆動開発(TDD、Test-Driven Development)で、「事前テスト第一」での開発をしていると、「機能単位別」にテストコードを構成して、そのテストコードを通過する実際のコードを作成したりしますね。あるいはその逆も可能です。このとき使用されるテストコードがユニットテストです。
このテストを実行するには、module-name/src/test/java/の下位にテストコードを記述します。

Androidコンポーネントをそのままユニットテストコード上で使用すると、テストは失敗します。ユニットテストで使用するAndroidコンポーネントは、android-stubs-src.jarファイルを参照しますが、ファイルは空のシェル(スタブ、stub)だけ集まっているため、実行することができません。
つまり、以下で説明するインストゥルメント化テストが必要になります。
(詳細は、[Kotlin]テストコード作成のための効率的なアプローチとはを参照)

インストゥルメント化テストは、実際のハードウェア機器やエミュレータで実行されるテストです。Androidの環境でテストするため、実際のInstrumentation APIにアクセスできます。Android環境で実行されているAndroidJUnitRunnerによって実行されます。

このテストでは、module-name/src/androidTest/java/の下位にテストコードを記述します。インストゥルメント化テストは、ユニットテストと比較して、その速度が遅いことが難点です。実際にアプリをビルドして、配布しつつ、実行させる過程がテストに含まれているからです。

では、そんなに遅い「インストゥルメント化テスト」をあえてするべきでしょうか?
ユニットテストでInstrumentation APIにアクセスできれば(そのAPIをモック(mock)できるフレームワークがある場合)、ユニットテストだけで十分ではないでしょうか?

Robolectric

上のような質問の回答になるのが、Robolectricフレームワークです。
Androidのフレームワークに依存性があるコードをユニットテストで作成できるのです。(shadowに変換して処理)
つまり、Robolectricは独自にandroid.jarの行動をShadowingします。一般的に、ユニットテストでAndroidのコードがあるとき、参照するandroid-stubs-src.jarを参照しません。

もう少し詳しく説明すると、Robolectricを利用したユニットテスト実行時、以下で説明するRobolectricTestRunnerがJVMをフッキングし、クラスローダーがAndroid コンポーネントをロードするのではなく、RobolectricのShadowオブジェクトをロードするようにします。

Robolectricは、実際のデバイスのセンサーの動作やエラー状況などをハンドリングしてくれるので、その機能に対して十分なユニットテストができるようになります。

例をあげてみましょう。

私たちが開発したButtonActivityは、内部のボタンをクリックすると、「Hello, Robolectric!」というフレーズがTextViewに表示されます。
これらのロジックは、次のようなButtonActivityTest ユニットテストから検証できます。

// ButtonActivityのロジック上、ボタンをクリックをするとTextViewテキストが「Hello,Robolectric!」に変更される。

@RunWith(RobolectricTestRunner::class)
class ButtonActivityTest {
    @Test
    fun `ボタンをクリックすると、TextView文言が変更される`() {
        // Activity Mocking
        val activity = Robolectric.setupActivity(ButtonActivity::class.java) // Activityを生成し、onCreateコールバックを呼び出す。
        val button = activity.findViewById<Button>(R.id.button) // Buttonリソースidを利用して、Buttonオブジェクトを持ってくる。
        val textView = activity.findViewById<TextView>(R.id.textView) // TextViewリソースidを利用して、TextViewオブジェクトを持ってくる。

        // Button Click
        button.performClick() // ボタンをクリックし、ユーザーの行動をシミュレーションする。

        // Test Code
        val expectedText = "Hello, Robolectric!" 
        val actualText = textView.text.toString() // 実際のTextViewのtextを持ってくる。
        assertEquals(expectedText, actualText) // 期待値と実際の結果が同じかチェックする。
    }
}

このように、ボタンの動作に対するユニットテストコードが簡単に作成できます。
これからRobolectricについて、詳しく見てみましょう。

1. Robolectricを開始する

Robolectricは、GradleとBazelで非常にうまく動作します。

Gradle設定

build.gradleに、次の構文を追加

includeAndroidResourcesから、実際のアプリに含まれるリソースをユニットテストパスも参照して使用することができます。

dependencies {
  // 書き込み時点を基準に最新バージョンです。
  testImplementation 'org.robolectric:robolectric:4.2'
}

android {
  testOptions {
    unitTests {
      includeAndroidResources = true
    }
  }
}

Test classにAnnotationを追加(RobolectricTestRunner)

@RunWith(RobolectricTestRunner::class)
class MainActivityTest {
    ...
}

2. 設定を変更する

Robolectricは、実行時に多くのアクションを設定/変更することができます。
robolectric.propertiesファイルをパッケージレベルのパスに置いたり、クラス/メソッドレベルで@Configアノテーションを使用して設定ができます。

@Config アノテーション

例えば、テストしようとする特定のクラスに@Configアノテーションをつけて、次のように設定できます。

@Config(
    sdk = { JELLYBEAN_MR1, KITKAT },
    manifest = "some/build/path/AndroidManifest.xml",
    application = MyCustomApplicatoin.class,
    resourceDir = "some/build/path/res",
    shadows = { ShadowFoo.class, ShadowBar.class } // Shadowについては、次章(3.Shadow?Shadow!)で扱います。
)
public class MainActivityTest {
    ...
    @Config(
        sdk = KITKAT,
        qualifiers = "fr-xlarge"
    )
    public void test_only_kitcat() {
        ...
    }
}

robolectic.properties ファイル

適用するパッケージのパスにrobolectric.propertiesファイルを作成します。
次のように作成すると、sdkは18(Android 4.3, JELLY_BEAN_MR2)で設定され、テストコードが実行されます。
もちろんマニフェストを設定して、「テスト用」manifestを設定することもできます。

# src/test/resources/com/mycompany/app/robolectric.properties
sdk=18
manifest=some/build/path/AndroidManifest.xml
shadows=my.package.ShadowFoo,my.package.ShadowBar

グローバル設定

同じ設定をパッケージ、クラス、メソッド別に実行するのは面倒な作業ですね。
この時間を短縮するため、Robolectric Test Runnerを継承してオーバーライドすることで、グローバル設定ができます。

class MyTestRunner: RobolectricTestRunner() {
    override fun buildGlobalConfig() {
        // TODO: グローバル用にここをオーバーライドする。
    }
}

上のようにCustom Robolectric Test Runnerを作成した後、Annotationを使ってMyTestRunnerをTest Runnerに設定します。

@RunWith(MyTestRunner.class)
public class MainActivityTest {
    ...
}

端末設定

上で示した、@ConfigAnnotationのqualifiersプロパティを用いて、アンドロイドの端末設定を操作することができます。

@RunWith(MyTestRunner.class)
@Config(qualifiers = "xlarge-port")
public class MainActivityTest {
  public void testItWithXlargePort() { ... } // config is "xlarge-port"

  @Config(qualifiers = "+land")
  public void testItWithXlargeLand() { ... } // config is "xlarge-land"

  @Config(qualifiers = "land")
  public void testItWithLand() { ... } // config is "normal-land"
}

このように、画面解像度、オリエンテーションなどが設定でき、これらを使ったテストも実行できます。

3. Shadow? Shadow!

すでに説明したように、RobolectricはAndroidランタイム環境を提供するので、ユニットテストの際にも「実際の端末で動作するような試験」ができます。
しかし、制限事項も存在します。

  1. ネイティブコード
    • Android ネイティブコードは、Robolectric(テストが実行されている開発機)環境で実行することができません。
  2. システムコールの範囲外
    • Robolectricが動いている開発機内で、AndroidのSystem Serviceが起動しているわけではありません。
  3. テスト用APIが不適切
    • Androidにはテスト利用に適したAPIがほとんどないと言えます。

このような制約を補うため、RobolectricはShadow(シャドウ)というクラスを提供しています。Shadowは、Androidクラスに合わせて、その動作を拡張し、変更することができます。Androidクラスが初期化されると、Robolecticは、適切なShadow クラスを見つけて作成し、接続します。

Robolectricは、バイトコードを操作することで、ネイティブコードとテスト可能なAPIに対する仮実装ができます。

Shadow? 名前の意味

Shadow(シャドウ)はProxyパターンではなくフェイクオブジェクトでもありません。また、モックやスタブでもありません。Shadowは隠されたり、時には表示されたりします。そして、実際のオブジェクトを指すこともあります。このようなことから、Shadowという名前が付けられました。

Shadowクラス

Shadowクラスは、引数がないpublicコンストラクタを必要とします。このコンストラクタを使ってRobolectricはShadowクラスを作成します。
生成されたクラスは、@Implementsアノテーションが付いたクラスと連結して動作します。

// TextViewに対するShadowクラス
@Implements(TextView.class)
public class MyShadowTextView extends ShadowView {
    // 次のように宣言をするか、でなければ宣言をしない。 コンパイラが基本生成子を自動で生成するため。)
    public MyShadowViewGroup() {
        super();
    }
}

メソッドをShadowing

ShadowオブジェクトはAndroidクラスと同じシグニチャ(signature)を持ったメソッドを実装する必要があります。Androidオブジェクトで同じシグネチャをもつメソッドが呼び出されると、Robolectricは該当のShadowメソッドを呼び出します。
メソッドには、@Implementationアノテーションを付けます。(protected modifierを付けなければならないことに留意してください)

例えば、次のようなアプリケーションロジックがあるとしましょう。

...
myTextView.setText("Hello");
...

TextViewに対するShadowとShadowメソッドを作成します。

// TextViewに対するShadowクラス
@Implements(TextView.class)
public class MyShadowTextView extends ShadowView {

    @Implementation
    protected void setText(CharSequence text) {
        // TODO: Shadowing実装
    }
}

興味深いのは、Robolectricが元のクラスのpublic, protected, private, static, final, nativemodifierをすべてShadowingできる点です。

注意!Shadowを書くときに留意すべき点が1つあります。
Shadow対象となるメソッドは、メソッドが定義されている元のクラスと符合するShadowingクラスで使用する必要があります。

例えば、setEnabled()メソッドは、Viewクラスに定義されています。もし、setEnabled()メソッドがShadowViewの代わりにShadowViewGroupを継承したクラスの@Implementationとして使用されている場合、Robolectricは、実行時にそのメソッドを検出できません。
ViewGroupもsetEnabled()メソッドが定義されていますが、元のクラスがないからです。

コンストラクタをShadowing

コンストラクタもShadowingができます。メソッドをShadowingと同じシグニチャで作成します。
その代わりメソッド名は、__constructor__でなければならないということに注意してください。

例をあげましょう。

new TextView(context);

上のようなTextViewに対するShadowクラスとコンストラクタを見てみましょう。

// TextViewに対するShadowクラス
@Implements(TextView.class)
public class MyShadowTextView extends ShadowView {

    @Implementation
    protected void __constructor__(Context context) {
        this.context = context;
    }
}

Shadowの詳しい説明は、以下のリンクを参照してください。

4.ベストプラクティス

Robolectricでは、次の4つを 最良の活用事例として提示しています。

  • Don’t
    • 他のAndroid codeの動作によって変更されるAndroidのクラスを、モックしたり、スパイしたりしないでください。(例えば、Context、SharedPreferencesのようなクラスがあります。)
    • 明確で小さな責任だけを持つ一部のイベントリスナーにのみ、モックやスパイを実施してください。
  • Do
    • RobolectricにLayout Inflationテストを行うとき、ActivityとLayout間の相互作用が直接行われるように確認し、クリックリスナーを正しくセットしてください。
    • LayoutInflaterをモックしたり、またはViewの抽象化を用いてテストしたりしないでください。
  • Do
    • public Lifecycle APIs(例. Robolectric.buildActivity()を通じて提供されるAPI)を使用してください。
    • アクティビティやサービスなどのAndroidのコンポーネントをテストするとき、@VisibleForTestingを利用したテスト目的のメソッドを利用しないでください。
    • @VisibleForTestingといったテスト目的のメソッドは、今後、テストコードをリファクタリングするとき、大きな困難が予想されます。
  • Do 
    • 各テストの進行中、最大スレッド数を制限してください。テストの間にガベージコレクションされていないスレッドが残り、テスト環境が汚染される可能性があります。
    • サードバーティのライブラリやコンポーネントを利用すると、スレッドが生成されることがありますが、このようにスレッドを生成するコンポーネントはできるかぎりモックしてください。DirectExecutorを使用することもできます。
    • 複数スレッドをテスト時に使用する必要があれば、明示的にすべてのスレッドとExecutorServiceを停止し、テストを行ってください。

まとめ

Spring Frameworkの開発をするとき、たくさんのモックとスタブを作成しなければならないケースがあるでしょう。その場合、複数の環境を模写して、テストをすることになりますが、Androidでは開発環境でAndroidランタイムを再現するのが難しく、QAにテストを依頼したり、開発者テストで代替することが多かったと思います。
しかし、Robolectricを使うと、以前よりもより速く、より信頼性の高い機能のユニットテストを作成できるようになります。
Robolectricをあなたのプロジェクトにも適用して、プロジェクトの安定性を強化してみてはいかがでしょうか?

Reference

NHN Cloud Meetup 編集部

NHN Cloudの技術ナレッジやお得なイベント情報を発信していきます
pagetop